defmodule BDS.CSM006NPlusOneTest do
  @moduledoc """
  Tests for CSM-006: N+1 Queries in Reindexing & Rendering.

  Verifies that reindex_posts/1, reindex_media/1, and normalize_list_posts
  use batched queries instead of per-record queries.
  """
  use ExUnit.Case, async: false

  alias BDS.Media.Media
  alias BDS.Repo

  setup do
    :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
    temp_dir = Path.join(System.tmp_dir!(), "bds-csm006-#{System.unique_integer([:positive])}")
    File.mkdir_p!(temp_dir)
    on_exit(fn -> File.rm_rf(temp_dir) end)

    {:ok, project} = BDS.Projects.create_project(%{name: "CSM006", data_path: temp_dir})
    %{project: project, temp_dir: temp_dir}
  end

  describe "reindex_posts/1 correctness with batched translations" do
    test "reindexes posts with translations correctly", %{project: project} do
      # Create posts, some with translations
      posts =
        for i <- 1..5 do
          {:ok, post} =
            BDS.Posts.create_post(%{
              project_id: project.id,
              title: "Post #{i}",
              content: "Content for post #{i}",
              tags: ["tag#{i}"],
              categories: ["cat#{i}"],
              language: "en"
            })

          if rem(i, 2) == 0 do
            BDS.Posts.Translations.upsert_post_translation(post.id, "de", %{
              title: "Beitrag #{i}",
              content: "Inhalt für Beitrag #{i}"
            })

            BDS.Posts.Translations.upsert_post_translation(post.id, "fr", %{
              title: "Article #{i}",
              content: "Contenu pour article #{i}"
            })
          end

          post
        end

      # Clear FTS and reindex
      Repo.query!(
        "DELETE FROM posts_fts WHERE post_id IN (SELECT id FROM posts WHERE project_id = ?)",
        [project.id]
      )

      # Reindex should succeed and produce correct FTS entries
      assert :ok = BDS.Search.reindex_posts(project.id)

      # Verify all posts are indexed
      %{rows: rows} = Repo.query!("SELECT post_id FROM posts_fts ORDER BY post_id")
      indexed_ids = Enum.map(rows, fn [id] -> id end) |> Enum.sort()
      expected_ids = Enum.map(posts, & &1.id) |> Enum.sort()
      assert indexed_ids == expected_ids

      # Verify translation content is in the index (search for German title)
      {:ok, results} = BDS.Search.search_posts(project.id, "Beitrag")
      assert results.total >= 1

      # Verify search for French content works
      {:ok, results} = BDS.Search.search_posts(project.id, "Contenu")
      assert results.total >= 1
    end

    test "reindex handles posts with no translations", %{project: project} do
      {:ok, post} =
        BDS.Posts.create_post(%{
          project_id: project.id,
          title: "Solo Post",
          content: "Solo content unique-keyword-xyz",
          tags: ["solo"],
          categories: [],
          language: "en"
        })

      Repo.query!(
        "DELETE FROM posts_fts WHERE post_id IN (SELECT id FROM posts WHERE project_id = ?)",
        [project.id]
      )

      assert :ok = BDS.Search.reindex_posts(project.id)

      {:ok, results} = BDS.Search.search_posts(project.id, "unique-keyword-xyz")
      assert results.total == 1
      assert hd(results.posts).id == post.id
    end
  end

  describe "reindex_media/1 correctness with batched translations" do
    test "reindexes media with translations correctly", %{project: project} do
      now = System.os_time(:second)

      media_items =
        for i <- 1..5 do
          {:ok, media} =
            %Media{}
            |> Media.changeset(%{
              id: Ecto.UUID.generate(),
              project_id: project.id,
              title: "Media #{i}",
              alt: "Alt text #{i}",
              caption: "Caption #{i}",
              original_name: "file#{i}.jpg",
              filename: "file#{i}.jpg",
              mime_type: "image/jpeg",
              size: 1024,
              file_path: "media/file#{i}.jpg",
              sidecar_path: "media/file#{i}.json",
              tags: ["media_tag#{i}"],
              language: "en",
              created_at: now,
              updated_at: now
            })
            |> Repo.insert()

          if rem(i, 2) == 0 do
            %BDS.Media.Translation{}
            |> BDS.Media.Translation.changeset(%{
              id: Ecto.UUID.generate(),
              project_id: project.id,
              translation_for: media.id,
              language: "de",
              title: "Medien #{i}",
              alt: "Alt DE #{i}",
              caption: "Beschriftung #{i}",
              created_at: now,
              updated_at: now
            })
            |> Repo.insert!()
          end

          media
        end

      # Reindex
      assert :ok = BDS.Search.reindex_media(project.id)

      # Verify all media are indexed
      %{rows: rows} = Repo.query!("SELECT media_id FROM media_fts ORDER BY media_id")
      indexed_ids = Enum.map(rows, fn [id] -> id end) |> Enum.sort()
      expected_ids = Enum.map(media_items, & &1.id) |> Enum.sort()
      assert indexed_ids == expected_ids

      # Verify translation content is in the index
      {:ok, results} = BDS.Search.search_media(project.id, "Medien")
      assert results.total >= 1

      {:ok, results} = BDS.Search.search_media(project.id, "Beschriftung")
      assert results.total >= 1
    end
  end

  describe "ListArchive batch post loading" do
    test "list_assigns loads post records in batch", %{project: project} do
      # Create posts
      posts =
        for i <- 1..5 do
          {:ok, post} =
            BDS.Posts.create_post(%{
              project_id: project.id,
              title: "List Post #{i}",
              content: "List content #{i}",
              tags: ["list_tag"],
              categories: ["list_cat"],
              language: "en"
            })

          post
        end

      # Pass post assigns with only IDs (forces load_post_record)
      post_assigns =
        Enum.map(posts, fn post ->
          %{id: post.id, slug: post.slug, title: post.title}
        end)

      # Should succeed and return properly enriched post data
      result =
        BDS.Rendering.ListArchive.list_assigns(project.id, %{
          posts: post_assigns,
          language: "en"
        })

      assert is_map(result)
      assert length(result.posts) == 5

      # Verify that post records were loaded (fallback fields like author come from DB)
      # Each post in results should have enriched data from the DB record
      for post <- result.posts do
        assert is_binary(post.id)
        assert is_binary(post.slug)
        assert is_binary(post.title)
      end
    end

    test "PostRendering.load_post_records_batch/1 returns map of id to record", %{
      project: project
    } do
      posts =
        for i <- 1..3 do
          {:ok, post} =
            BDS.Posts.create_post(%{
              project_id: project.id,
              title: "Batch #{i}",
              content: "Batch content #{i}",
              tags: [],
              categories: [],
              language: "en"
            })

          post
        end

      ids = Enum.map(posts, & &1.id)
      records_map = BDS.Rendering.PostRendering.load_post_records_batch(ids)

      assert map_size(records_map) == 3

      for post <- posts do
        assert Map.has_key?(records_map, post.id)
        assert records_map[post.id].title == post.title
      end
    end

    test "PostRendering.load_post_records_batch/1 handles empty list" do
      assert BDS.Rendering.PostRendering.load_post_records_batch([]) == %{}
    end

    test "PostRendering.load_post_records_batch/1 handles nonexistent IDs" do
      records_map = BDS.Rendering.PostRendering.load_post_records_batch(["nonexistent-id"])
      assert records_map == %{"nonexistent-id" => nil}
    end
  end
end
