defmodule Berrypod.SearchTest do
  use Berrypod.DataCase, async: false

  alias Berrypod.Search

  import Berrypod.ProductsFixtures

  setup do
    conn = provider_connection_fixture()

    mountain =
      product_fixture(%{
        provider_connection: conn,
        title: "Mountain Sunrise Art Print",
        description: "<p>A beautiful <strong>mountain</strong> landscape at dawn.</p>",
        category: "Art Prints"
      })

    product_variant_fixture(%{
      product: mountain,
      title: "Small / Navy",
      options: %{"Size" => "Small", "Color" => "Navy"},
      price: 1999
    })

    ocean =
      product_fixture(%{
        provider_connection: conn,
        title: "Ocean Waves Notebook",
        description: "A spiral-bound notebook with ocean wave cover art.",
        category: "Stationery"
      })

    product_variant_fixture(%{
      product: ocean,
      title: "A5",
      options: %{"Size" => "A5"},
      price: 1299
    })

    forest =
      product_fixture(%{
        provider_connection: conn,
        title: "Forest Silhouette T-Shirt",
        description: "Cotton t-shirt with forest silhouette print.",
        category: "Apparel"
      })

    product_variant_fixture(%{
      product: forest,
      title: "Large / Black",
      options: %{"Size" => "Large", "Color" => "Black"},
      price: 2999
    })

    product_variant_fixture(%{
      product: forest,
      title: "Medium / Navy Blue",
      options: %{"Size" => "Medium", "Color" => "Navy Blue"},
      price: 2999
    })

    # Hidden product — should not appear in search
    _hidden =
      product_fixture(%{
        provider_connection: conn,
        title: "Hidden Mountain Poster",
        description: "This should not be searchable.",
        category: "Art Prints",
        visible: false
      })

    Search.rebuild_index()

    %{mountain: mountain, ocean: ocean, forest: forest}
  end

  describe "search/1" do
    test "finds products by title", %{mountain: mountain} do
      results = Search.search("mountain")
      assert length(results) >= 1
      assert Enum.any?(results, &(&1.id == mountain.id))
    end

    test "finds products by category", %{mountain: mountain, ocean: ocean} do
      results = Search.search("art prints")
      assert Enum.any?(results, &(&1.id == mountain.id))
      refute Enum.any?(results, &(&1.id == ocean.id))
    end

    test "finds products by variant attributes", %{forest: forest} do
      results = Search.search("navy")
      assert Enum.any?(results, &(&1.id == forest.id))
    end

    test "finds products by description", %{mountain: mountain} do
      results = Search.search("landscape")
      assert Enum.any?(results, &(&1.id == mountain.id))
    end

    test "prefix matching on last token", %{mountain: mountain} do
      results = Search.search("mou")
      assert Enum.any?(results, &(&1.id == mountain.id))
    end

    test "multi-word query matches all tokens", %{mountain: mountain} do
      results = Search.search("mountain sunrise")
      assert Enum.any?(results, &(&1.id == mountain.id))
    end

    test "returns empty list for blank query" do
      assert Search.search("") == []
      assert Search.search("  ") == []
    end

    test "returns empty list for single character" do
      assert Search.search("a") == []
    end

    test "returns empty list for nil" do
      assert Search.search(nil) == []
    end

    test "returns empty list for no matches" do
      assert Search.search("xyznonexistent") == []
    end

    test "special characters don't crash" do
      assert Search.search("mountain's") == Search.search("mountains")
      assert Search.search("test & foo") == []
      assert Search.search("(brackets)") == []
    end

    test "hidden products are excluded" do
      results = Search.search("hidden")
      assert results == []
    end

    test "strips HTML from descriptions", %{mountain: mountain} do
      results = Search.search("landscape dawn")
      assert Enum.any?(results, &(&1.id == mountain.id))
    end

    test "results include listing preloads", %{mountain: mountain} do
      [result | _] = Search.search("mountain sunrise")
      assert result.id == mountain.id
      assert Ecto.assoc_loaded?(result.images)
    end

    test "title matches rank higher than description matches" do
      results = Search.search("mountain")
      # Mountain Sunrise Art Print (title match) should rank above
      # Forest T-Shirt if it happened to mention "mountain" in description
      first = List.first(results)
      assert first.title =~ "Mountain"
    end
  end

  describe "rebuild_index/0" do
    test "rebuilds from scratch" do
      # Verify search works before rebuild
      assert length(Search.search("mountain")) >= 1

      # Rebuild and verify still works
      Search.rebuild_index()
      assert length(Search.search("mountain")) >= 1
    end
  end

  describe "index_product/1" do
    test "indexes a single product", %{ocean: ocean} do
      # Clear FTS index
      Berrypod.Repo.query!("DELETE FROM products_search_map")
      Berrypod.Repo.query!("DELETE FROM products_search")

      # Verify FTS index is empty
      %{rows: rows} = Berrypod.Repo.query!("SELECT COUNT(*) FROM products_search_map")
      assert rows == [[0]]

      # Index just ocean
      ocean = Berrypod.Repo.preload(ocean, [:variants])
      Search.index_product(ocean)

      # Verify ocean is now in the FTS index
      %{rows: [[count]]} = Berrypod.Repo.query!("SELECT COUNT(*) FROM products_search_map")
      assert count == 1

      results = Search.search("ocean")
      assert length(results) >= 1
      assert hd(results).id == ocean.id
    end

    test "reindexing updates existing entry", %{mountain: mountain} do
      # Change title and reindex
      mountain =
        mountain
        |> Ecto.Changeset.change(title: "Alpine Sunrise Art Print")
        |> Berrypod.Repo.update!()

      Search.index_product(mountain)

      assert Search.search("alpine") != []
    end
  end

  describe "LIKE fallback" do
    test "finds substring matches that FTS5 prefix misses", %{ocean: ocean} do
      # "ebook" is in the middle of "Notebook" — FTS5 prefix won't match
      results = Search.search("ebook")
      assert Enum.any?(results, &(&1.id == ocean.id))
    end

    test "falls back to category substring match", %{forest: forest} do
      # "ppar" is a substring of "Apparel" — FTS5 prefix won't match
      results = Search.search("pparel")
      assert Enum.any?(results, &(&1.id == forest.id))
    end
  end

  describe "remove_product/1" do
    test "removes a product from the index", %{ocean: ocean} do
      # Verify ocean is in the FTS index
      %{rows: [[rowid]]} =
        Berrypod.Repo.query!(
          "SELECT rowid FROM products_search_map WHERE product_id = ?1",
          [ocean.id]
        )

      assert rowid

      Search.remove_product(ocean.id)

      # Verify it's gone from the FTS index
      %{rows: rows} =
        Berrypod.Repo.query!(
          "SELECT rowid FROM products_search_map WHERE product_id = ?1",
          [ocean.id]
        )

      assert rows == []
    end

    test "is a no-op for unindexed product" do
      assert Search.remove_product(-1) == :ok
    end
  end
end
