defmodule SimpleshopTheme.ProductsTest do
  use SimpleshopTheme.DataCase, async: false

  alias SimpleshopTheme.Products
  alias SimpleshopTheme.Products.{ProviderConnection, Product, ProductImage, ProductVariant}

  import SimpleshopTheme.ProductsFixtures

  # =============================================================================
  # Provider Connections
  # =============================================================================

  describe "list_provider_connections/0" do
    test "returns empty list when no connections exist" do
      assert Products.list_provider_connections() == []
    end

    test "returns all provider connections" do
      conn1 = provider_connection_fixture(%{provider_type: "printify"})
      conn2 = provider_connection_fixture(%{provider_type: "gelato"})

      connections = Products.list_provider_connections()
      assert length(connections) == 2
      assert Enum.any?(connections, &(&1.id == conn1.id))
      assert Enum.any?(connections, &(&1.id == conn2.id))
    end
  end

  describe "get_provider_connection/1" do
    test "returns the connection with given id" do
      conn = provider_connection_fixture()
      assert Products.get_provider_connection(conn.id).id == conn.id
    end

    test "returns nil for non-existent id" do
      assert Products.get_provider_connection(Ecto.UUID.generate()) == nil
    end
  end

  describe "get_provider_connection_by_type/1" do
    test "returns the connection with given provider_type" do
      conn = provider_connection_fixture(%{provider_type: "printify"})
      assert Products.get_provider_connection_by_type("printify").id == conn.id
    end

    test "returns nil for non-existent type" do
      assert Products.get_provider_connection_by_type("nonexistent") == nil
    end
  end

  describe "create_provider_connection/1" do
    test "creates a provider connection with valid attrs" do
      attrs = valid_provider_connection_attrs()
      assert {:ok, %ProviderConnection{} = conn} = Products.create_provider_connection(attrs)

      assert conn.provider_type == attrs.provider_type
      assert conn.name == attrs.name
      assert conn.enabled == true
    end

    test "returns error changeset with invalid attrs" do
      assert {:error, %Ecto.Changeset{}} = Products.create_provider_connection(%{})
    end
  end

  describe "update_provider_connection/2" do
    test "updates the connection with valid attrs" do
      conn = provider_connection_fixture()
      assert {:ok, updated} = Products.update_provider_connection(conn, %{name: "Updated Name"})
      assert updated.name == "Updated Name"
    end

    test "returns error changeset with invalid attrs" do
      conn = provider_connection_fixture()

      assert {:error, %Ecto.Changeset{}} =
               Products.update_provider_connection(conn, %{provider_type: "invalid"})
    end
  end

  describe "delete_provider_connection/1" do
    test "deletes the connection" do
      conn = provider_connection_fixture()
      assert {:ok, %ProviderConnection{}} = Products.delete_provider_connection(conn)
      assert Products.get_provider_connection(conn.id) == nil
    end
  end

  describe "update_sync_status/3" do
    test "updates sync status" do
      conn = provider_connection_fixture()
      assert {:ok, updated} = Products.update_sync_status(conn, "syncing")
      assert updated.sync_status == "syncing"
    end

    test "updates sync status with timestamp" do
      conn = provider_connection_fixture()
      now = DateTime.utc_now() |> DateTime.truncate(:second)
      assert {:ok, updated} = Products.update_sync_status(conn, "completed", now)

      assert updated.sync_status == "completed"
      assert updated.last_synced_at == now
    end
  end

  # =============================================================================
  # Products
  # =============================================================================

  describe "list_products/1" do
    test "returns empty list when no products exist" do
      assert Products.list_products() == []
    end

    test "returns all products" do
      product1 = product_fixture()
      product2 = product_fixture()

      products = Products.list_products()
      assert length(products) == 2
      ids = Enum.map(products, & &1.id)
      assert product1.id in ids
      assert product2.id in ids
    end

    test "filters by visible" do
      _visible = product_fixture(%{visible: true})
      _hidden = product_fixture(%{visible: false})

      visible_products = Products.list_products(visible: true)
      assert length(visible_products) == 1
      assert hd(visible_products).visible == true
    end

    test "filters by status" do
      _active = product_fixture(%{status: "active"})
      _draft = product_fixture(%{status: "draft"})

      active_products = Products.list_products(status: "active")
      assert length(active_products) == 1
      assert hd(active_products).status == "active"
    end

    test "filters by category" do
      _apparel = product_fixture(%{category: "Apparel"})
      _homewares = product_fixture(%{category: "Homewares"})

      apparel_products = Products.list_products(category: "Apparel")
      assert length(apparel_products) == 1
      assert hd(apparel_products).category == "Apparel"
    end

    test "filters by provider_connection_id" do
      conn1 = provider_connection_fixture(%{provider_type: "printify"})
      conn2 = provider_connection_fixture(%{provider_type: "gelato"})

      product1 = product_fixture(%{provider_connection: conn1})
      _product2 = product_fixture(%{provider_connection: conn2})

      products = Products.list_products(provider_connection_id: conn1.id)
      assert length(products) == 1
      assert hd(products).id == product1.id
    end

    test "preloads associations" do
      product = product_fixture()
      _image = product_image_fixture(%{product: product})
      _variant = product_variant_fixture(%{product: product})

      [loaded] = Products.list_products(preload: [:images, :variants])
      assert length(loaded.images) == 1
      assert length(loaded.variants) == 1
    end
  end

  describe "get_product/2" do
    test "returns the product with given id" do
      product = product_fixture()
      assert Products.get_product(product.id).id == product.id
    end

    test "returns nil for non-existent id" do
      assert Products.get_product(Ecto.UUID.generate()) == nil
    end

    test "preloads associations when requested" do
      product = product_fixture()
      _image = product_image_fixture(%{product: product})

      loaded = Products.get_product(product.id, preload: [:images])
      assert length(loaded.images) == 1
    end
  end

  describe "get_product_by_slug/2" do
    test "returns the product with given slug" do
      product = product_fixture(%{slug: "my-product"})
      assert Products.get_product_by_slug("my-product").id == product.id
    end

    test "returns nil for non-existent slug" do
      assert Products.get_product_by_slug("nonexistent") == nil
    end
  end

  describe "get_product_by_provider/2" do
    test "returns the product by provider connection and product id" do
      conn = provider_connection_fixture()
      product = product_fixture(%{provider_connection: conn, provider_product_id: "ext_123"})

      assert Products.get_product_by_provider(conn.id, "ext_123").id == product.id
    end

    test "returns nil when not found" do
      conn = provider_connection_fixture()
      assert Products.get_product_by_provider(conn.id, "nonexistent") == nil
    end
  end

  describe "create_product/1" do
    test "creates a product with valid attrs" do
      conn = provider_connection_fixture()
      attrs = valid_product_attrs(%{provider_connection_id: conn.id})

      assert {:ok, %Product{} = product} = Products.create_product(attrs)
      assert product.title == attrs.title
      assert product.provider_product_id == attrs.provider_product_id
    end

    test "returns error changeset with invalid attrs" do
      assert {:error, %Ecto.Changeset{}} = Products.create_product(%{})
    end
  end

  describe "update_product/2" do
    test "updates the product with valid attrs" do
      product = product_fixture()
      assert {:ok, updated} = Products.update_product(product, %{title: "Updated Title"})
      assert updated.title == "Updated Title"
    end
  end

  describe "delete_product/1" do
    test "deletes the product" do
      product = product_fixture()
      assert {:ok, %Product{}} = Products.delete_product(product)
      assert Products.get_product(product.id) == nil
    end
  end

  describe "upsert_product/2" do
    test "creates new product when not exists" do
      conn = provider_connection_fixture()

      attrs = %{
        provider_product_id: "new_ext_123",
        title: "New Product",
        slug: "new-product",
        provider_data: %{"key" => "value"}
      }

      assert {:ok, product, :created} = Products.upsert_product(conn, attrs)
      assert product.title == "New Product"
      assert product.provider_connection_id == conn.id
    end

    test "updates existing product when checksum differs" do
      conn = provider_connection_fixture()
      existing = product_fixture(%{provider_connection: conn, provider_product_id: "ext_123"})

      attrs = %{
        provider_product_id: "ext_123",
        title: "Updated Title",
        slug: existing.slug,
        provider_data: %{"different" => "data"}
      }

      assert {:ok, product, :updated} = Products.upsert_product(conn, attrs)
      assert product.id == existing.id
      assert product.title == "Updated Title"
    end

    test "returns unchanged when checksum matches" do
      conn = provider_connection_fixture()
      provider_data = %{"key" => "value"}

      existing =
        product_fixture(%{
          provider_connection: conn,
          provider_product_id: "ext_123",
          provider_data: provider_data,
          checksum: Product.compute_checksum(provider_data)
        })

      attrs = %{
        provider_product_id: "ext_123",
        title: "Different Title",
        slug: existing.slug,
        provider_data: provider_data
      }

      assert {:ok, product, :unchanged} = Products.upsert_product(conn, attrs)
      assert product.id == existing.id
      # Title should NOT be updated since checksum matched
      assert product.title == existing.title
    end
  end

  # =============================================================================
  # Product Images
  # =============================================================================

  describe "create_product_image/1" do
    test "creates a product image" do
      product = product_fixture()
      attrs = valid_product_image_attrs(%{product_id: product.id})

      assert {:ok, %ProductImage{} = image} = Products.create_product_image(attrs)
      assert image.product_id == product.id
    end
  end

  describe "delete_product_images/1" do
    test "deletes all images for a product" do
      product = product_fixture()
      _image1 = product_image_fixture(%{product: product})
      _image2 = product_image_fixture(%{product: product})

      assert {2, nil} = Products.delete_product_images(product)

      loaded = Products.get_product(product.id, preload: [:images])
      assert loaded.images == []
    end
  end

  describe "sync_product_images/2" do
    test "replaces all images" do
      product = product_fixture()
      _old_image = product_image_fixture(%{product: product})

      new_images = [
        %{src: "https://new.com/1.jpg"},
        %{src: "https://new.com/2.jpg"}
      ]

      results = Products.sync_product_images(product, new_images)
      assert length(results) == 2
      assert Enum.all?(results, &match?({:ok, _}, &1))

      loaded = Products.get_product(product.id, preload: [:images])
      assert length(loaded.images) == 2
    end

    test "assigns positions based on list order" do
      product = product_fixture()

      images = [
        %{src: "https://new.com/first.jpg"},
        %{src: "https://new.com/second.jpg"}
      ]

      Products.sync_product_images(product, images)

      loaded = Products.get_product(product.id, preload: [:images])
      sorted = Enum.sort_by(loaded.images, & &1.position)

      assert Enum.at(sorted, 0).position == 0
      assert Enum.at(sorted, 1).position == 1
    end
  end

  # =============================================================================
  # Product Variants
  # =============================================================================

  describe "create_product_variant/1" do
    test "creates a product variant" do
      product = product_fixture()
      attrs = valid_product_variant_attrs(%{product_id: product.id})

      assert {:ok, %ProductVariant{} = variant} = Products.create_product_variant(attrs)
      assert variant.product_id == product.id
    end
  end

  describe "update_product_variant/2" do
    test "updates the variant" do
      variant = product_variant_fixture()
      assert {:ok, updated} = Products.update_product_variant(variant, %{price: 3000})
      assert updated.price == 3000
    end
  end

  describe "delete_product_variants/1" do
    test "deletes all variants for a product" do
      product = product_fixture()
      _variant1 = product_variant_fixture(%{product: product})
      _variant2 = product_variant_fixture(%{product: product})

      assert {2, nil} = Products.delete_product_variants(product)

      loaded = Products.get_product(product.id, preload: [:variants])
      assert loaded.variants == []
    end
  end

  describe "get_variant_by_provider/2" do
    test "returns variant by product and provider variant id" do
      product = product_fixture()
      variant = product_variant_fixture(%{product: product, provider_variant_id: "var_123"})

      assert Products.get_variant_by_provider(product.id, "var_123").id == variant.id
    end

    test "returns nil when not found" do
      product = product_fixture()
      assert Products.get_variant_by_provider(product.id, "nonexistent") == nil
    end
  end

  # =============================================================================
  # Storefront queries
  # =============================================================================

  describe "recompute_cached_fields/1" do
    test "computes cheapest price from available variants" do
      product = product_fixture()

      product_variant_fixture(%{
        product: product,
        price: 3000,
        is_enabled: true,
        is_available: true
      })

      product_variant_fixture(%{
        product: product,
        price: 2000,
        is_enabled: true,
        is_available: true
      })

      product_variant_fixture(%{
        product: product,
        price: 1500,
        is_enabled: false,
        is_available: true
      })

      assert {:ok, updated} = Products.recompute_cached_fields(product)
      assert updated.cheapest_price == 2000
    end

    test "sets cheapest_price to 0 when no available variants" do
      product = product_fixture()

      product_variant_fixture(%{
        product: product,
        price: 2000,
        is_enabled: true,
        is_available: false
      })

      assert {:ok, updated} = Products.recompute_cached_fields(product)
      assert updated.cheapest_price == 0
    end

    test "sets in_stock based on available variants" do
      product = product_fixture()
      product_variant_fixture(%{product: product, is_enabled: true, is_available: true})

      assert {:ok, updated} = Products.recompute_cached_fields(product)
      assert updated.in_stock == true
    end

    test "sets in_stock false when no available variants" do
      product = product_fixture()
      product_variant_fixture(%{product: product, is_enabled: true, is_available: false})

      assert {:ok, updated} = Products.recompute_cached_fields(product)
      assert updated.in_stock == false
    end

    test "sets on_sale when any variant has compare_at_price > price" do
      product = product_fixture()
      product_variant_fixture(%{product: product, price: 2000, compare_at_price: 3000})

      assert {:ok, updated} = Products.recompute_cached_fields(product)
      assert updated.on_sale == true
    end

    test "sets on_sale false when no sale variants" do
      product = product_fixture()
      product_variant_fixture(%{product: product, price: 2000, compare_at_price: nil})

      assert {:ok, updated} = Products.recompute_cached_fields(product)
      assert updated.on_sale == false
    end

    test "stores compare_at_price from cheapest available variant" do
      product = product_fixture()

      product_variant_fixture(%{
        product: product,
        price: 2000,
        compare_at_price: 3000,
        is_enabled: true,
        is_available: true
      })

      product_variant_fixture(%{
        product: product,
        price: 1500,
        compare_at_price: 2500,
        is_enabled: true,
        is_available: true
      })

      assert {:ok, updated} = Products.recompute_cached_fields(product)
      assert updated.cheapest_price == 1500
      assert updated.compare_at_price == 2500
    end
  end

  describe "get_visible_product/1" do
    test "returns visible active product by slug" do
      product = product_fixture(%{slug: "test-product", visible: true, status: "active"})
      found = Products.get_visible_product("test-product")
      assert found.id == product.id
    end

    test "returns nil for hidden product" do
      _product = product_fixture(%{slug: "hidden", visible: false, status: "active"})
      assert Products.get_visible_product("hidden") == nil
    end

    test "returns nil for draft product" do
      _product = product_fixture(%{slug: "draft", visible: true, status: "draft"})
      assert Products.get_visible_product("draft") == nil
    end

    test "preloads images and variants" do
      product = product_fixture(%{slug: "preloaded"})
      product_image_fixture(%{product: product})
      product_variant_fixture(%{product: product})

      found = Products.get_visible_product("preloaded")
      assert length(found.images) == 1
      assert length(found.variants) == 1
    end
  end

  describe "list_visible_products/1" do
    test "returns only visible active products" do
      _visible = product_fixture(%{visible: true, status: "active"})
      _hidden = product_fixture(%{visible: false, status: "active"})
      _draft = product_fixture(%{visible: true, status: "draft"})

      products = Products.list_visible_products()
      assert length(products) == 1
    end

    test "filters by category" do
      _apparel = product_fixture(%{category: "Apparel"})
      _home = product_fixture(%{category: "Homewares"})

      products = Products.list_visible_products(category: "Apparel")
      assert length(products) == 1
      assert hd(products).category == "Apparel"
    end

    test "filters by on_sale" do
      sale = product_fixture()
      _regular = product_fixture()
      product_variant_fixture(%{product: sale, price: 2000, compare_at_price: 3000})
      Products.recompute_cached_fields(sale)

      products = Products.list_visible_products(on_sale: true)
      assert length(products) == 1
      assert hd(products).id == sale.id
    end

    test "filters by in_stock" do
      in_stock = product_fixture()
      out_of_stock = product_fixture()
      product_variant_fixture(%{product: in_stock, is_enabled: true, is_available: true})
      product_variant_fixture(%{product: out_of_stock, is_enabled: true, is_available: false})
      Products.recompute_cached_fields(in_stock)
      Products.recompute_cached_fields(out_of_stock)

      products = Products.list_visible_products(in_stock: true)
      assert length(products) == 1
      assert hd(products).id == in_stock.id
    end

    test "sorts by price ascending" do
      cheap = product_fixture()
      expensive = product_fixture()

      product_variant_fixture(%{
        product: cheap,
        price: 1000,
        is_enabled: true,
        is_available: true
      })

      product_variant_fixture(%{
        product: expensive,
        price: 5000,
        is_enabled: true,
        is_available: true
      })

      Products.recompute_cached_fields(cheap)
      Products.recompute_cached_fields(expensive)

      products = Products.list_visible_products(sort: "price_asc")
      assert Enum.map(products, & &1.id) == [cheap.id, expensive.id]
    end

    test "sorts by price descending" do
      cheap = product_fixture()
      expensive = product_fixture()

      product_variant_fixture(%{
        product: cheap,
        price: 1000,
        is_enabled: true,
        is_available: true
      })

      product_variant_fixture(%{
        product: expensive,
        price: 5000,
        is_enabled: true,
        is_available: true
      })

      Products.recompute_cached_fields(cheap)
      Products.recompute_cached_fields(expensive)

      products = Products.list_visible_products(sort: "price_desc")
      assert Enum.map(products, & &1.id) == [expensive.id, cheap.id]
    end

    test "sorts by name ascending" do
      b = product_fixture(%{title: "Banana"})
      a = product_fixture(%{title: "Apple"})

      products = Products.list_visible_products(sort: "name_asc")
      assert Enum.map(products, & &1.id) == [a.id, b.id]
    end

    test "limits results" do
      for _ <- 1..5, do: product_fixture()

      products = Products.list_visible_products(limit: 3)
      assert length(products) == 3
    end

    test "excludes a product by ID" do
      p1 = product_fixture()
      p2 = product_fixture()

      products = Products.list_visible_products(exclude: p1.id)
      assert length(products) == 1
      assert hd(products).id == p2.id
    end

    test "preloads images but not variants" do
      product = product_fixture()
      product_image_fixture(%{product: product})
      product_variant_fixture(%{product: product})

      [loaded] = Products.list_visible_products()
      assert length(loaded.images) == 1
      assert %Ecto.Association.NotLoaded{} = loaded.variants
    end
  end

  describe "list_categories/0" do
    test "returns distinct categories from visible products" do
      product_fixture(%{category: "Apparel"})
      product_fixture(%{category: "Homewares"})
      product_fixture(%{category: "Apparel"})
      product_fixture(%{category: nil})

      categories = Products.list_categories()
      assert length(categories) == 2
      assert Enum.map(categories, & &1.name) == ["Apparel", "Homewares"]
      assert Enum.map(categories, & &1.slug) == ["apparel", "homewares"]
    end

    test "excludes categories from hidden products" do
      product_fixture(%{category: "Visible", visible: true})
      product_fixture(%{category: "Hidden", visible: false})

      categories = Products.list_categories()
      assert length(categories) == 1
      assert hd(categories).name == "Visible"
    end
  end

  describe "sync_product_variants/2" do
    test "creates new variants" do
      product = product_fixture()

      variants = [
        %{provider_variant_id: "v1", title: "Small", price: 2000},
        %{provider_variant_id: "v2", title: "Large", price: 2500}
      ]

      results = Products.sync_product_variants(product, variants)
      assert length(results) == 2
      assert Enum.all?(results, &match?({:ok, _}, &1))

      loaded = Products.get_product(product.id, preload: [:variants])
      assert length(loaded.variants) == 2
    end

    test "updates existing variants" do
      product = product_fixture()

      existing =
        product_variant_fixture(%{product: product, provider_variant_id: "v1", price: 2000})

      variants = [
        %{provider_variant_id: "v1", title: "Small Updated", price: 2200}
      ]

      Products.sync_product_variants(product, variants)

      updated = Repo.get!(ProductVariant, existing.id)
      assert updated.title == "Small Updated"
      assert updated.price == 2200
    end

    test "removes variants not in incoming list" do
      product = product_fixture()
      _keep = product_variant_fixture(%{product: product, provider_variant_id: "keep"})
      _remove = product_variant_fixture(%{product: product, provider_variant_id: "remove"})

      variants = [
        %{provider_variant_id: "keep", title: "Keep", price: 2000}
      ]

      Products.sync_product_variants(product, variants)

      loaded = Products.get_product(product.id, preload: [:variants])
      assert length(loaded.variants) == 1
      assert hd(loaded.variants).provider_variant_id == "keep"
    end
  end
end
