defmodule BDS.TagsTest do
  use ExUnit.Case, async: false

  alias BDS.Posts.Post
  alias BDS.Repo

  setup do
    :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
    temp_dir = Path.join(System.tmp_dir!(), "bds-tags-#{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: "Tags", data_path: temp_dir})
    %{project: project, temp_dir: temp_dir}
  end

  test "create_tag persists the row and rewrites meta/tags.json sorted by name", %{
    project: project,
    temp_dir: temp_dir
  } do
    assert {:ok, zebra} =
             BDS.Tags.create_tag(%{project_id: project.id, name: "Zebra", color: "#000000"})

    assert {:ok, alpha} = BDS.Tags.create_tag(%{project_id: project.id, name: "Alpha"})

    assert zebra.name == "Zebra"
    assert zebra.color == "#000000"
    assert alpha.color == nil

    tags_path = Path.join([temp_dir, "meta", "tags.json"])
    assert File.exists?(tags_path)

    contents = File.read!(tags_path)
    assert contents =~ "\n"
    refute String.contains?(contents, "}],[")

    assert [%{"name" => "Alpha"}, %{"color" => "#000000", "name" => "Zebra"}] =
             Jason.decode!(contents)
  end

  test "create_tag rejects case-insensitive duplicates per project", %{project: project} do
    assert {:ok, _tag} = BDS.Tags.create_tag(%{project_id: project.id, name: "Elixir"})
    assert {:error, changeset} = BDS.Tags.create_tag(%{project_id: project.id, name: "elixir"})
    assert "has already been taken" in errors_on(changeset).name
  end

  test "update_tag rewrites the tag row and meta/tags.json", %{
    project: project,
    temp_dir: temp_dir
  } do
    assert {:ok, tag} = BDS.Tags.create_tag(%{project_id: project.id, name: "Alpha"})

    assert {:ok, updated} =
             BDS.Tags.update_tag(tag.id, %{
               color: "#112233",
               post_template_slug: "article"
             })

    assert updated.name == "Alpha"
    assert updated.color == "#112233"
    assert updated.post_template_slug == "article"
    assert updated.updated_at >= tag.updated_at

    tags_path = Path.join([temp_dir, "meta", "tags.json"])

    assert [
             %{"name" => "Alpha", "color" => "#112233", "postTemplateSlug" => "article"}
           ] =
             Jason.decode!(File.read!(tags_path))
  end

  test "rename_tag updates post tag arrays, rewrites published post files, and refreshes tags.json",
       %{project: project, temp_dir: temp_dir} do
    assert {:ok, tag} = BDS.Tags.create_tag(%{project_id: project.id, name: "Alpha"})

    assert {:ok, post} =
             BDS.Posts.create_post(%{
               project_id: project.id,
               title: "Tagged Post",
               content: "Body",
               tags: ["Alpha", "Other"]
             })

    assert {:ok, published_post} = BDS.Posts.publish_post(post.id)

    assert {:ok, renamed} = BDS.Tags.rename_tag(tag.id, "Beta")
    assert renamed.name == "Beta"

    reloaded_post = Repo.get!(Post, published_post.id)
    assert reloaded_post.tags == ["Beta", "Other"]

    post_path = Path.join(temp_dir, reloaded_post.file_path)
    contents = File.read!(post_path)
    assert contents =~ "tags:\n  - Beta\n  - Other\n"
    assert contents =~ "\n---\nBody\n"

    tags_path = Path.join([temp_dir, "meta", "tags.json"])
    assert [%{"name" => "Beta"}] = Jason.decode!(File.read!(tags_path))
  end

  test "rename_tag keeps committed database changes when tags json flush fails", %{
    project: project,
    temp_dir: temp_dir
  } do
    assert {:ok, tag} = BDS.Tags.create_tag(%{project_id: project.id, name: "Alpha"})

    assert {:ok, post} =
             BDS.Posts.create_post(%{
               project_id: project.id,
               title: "Tagged Draft",
               content: "Body",
               tags: ["Alpha"]
             })

    meta_path = Path.join(temp_dir, "meta")
    File.rm_rf!(meta_path)
    File.write!(meta_path, "not a directory")

    assert {:error, _reason} = BDS.Tags.rename_tag(tag.id, "Beta")

    assert Repo.get!(BDS.Tags.Tag, tag.id).name == "Beta"
    assert Repo.get!(Post, post.id).tags == ["Beta"]
  end

  test "merge_tags moves source tags onto the target, deduplicates post tags, deletes sources, and refreshes tags.json",
       %{project: project, temp_dir: temp_dir} do
    assert {:ok, source_a} = BDS.Tags.create_tag(%{project_id: project.id, name: "Alpha"})
    assert {:ok, source_b} = BDS.Tags.create_tag(%{project_id: project.id, name: "Beta"})
    assert {:ok, target} = BDS.Tags.create_tag(%{project_id: project.id, name: "Gamma"})

    assert {:ok, post} =
             BDS.Posts.create_post(%{
               project_id: project.id,
               title: "Merge Me",
               content: "Body",
               tags: ["Alpha", "Beta", "Gamma"]
             })

    assert {:ok, published_post} = BDS.Posts.publish_post(post.id)

    assert {:ok, :merged} = BDS.Tags.merge_tags([source_a.id, source_b.id], target.id)

    reloaded_post = Repo.get!(Post, published_post.id)
    assert reloaded_post.tags == ["Gamma"]
    assert Repo.get(BDS.Tags.Tag, source_a.id) == nil
    assert Repo.get(BDS.Tags.Tag, source_b.id) == nil
    assert Repo.get(BDS.Tags.Tag, target.id) != nil

    post_path = Path.join(temp_dir, reloaded_post.file_path)
    contents = File.read!(post_path)
    assert contents =~ "tags:\n  - Gamma\n"
    assert contents =~ "\n---\nBody\n"

    tags_path = Path.join([temp_dir, "meta", "tags.json"])
    assert [%{"name" => "Gamma"}] = Jason.decode!(File.read!(tags_path))
  end

  test "delete_tag removes the tag from posts, rewrites published files, deletes the row, and refreshes tags.json",
       %{project: project, temp_dir: temp_dir} do
    assert {:ok, doomed} = BDS.Tags.create_tag(%{project_id: project.id, name: "Alpha"})
    assert {:ok, _other} = BDS.Tags.create_tag(%{project_id: project.id, name: "Beta"})

    assert {:ok, post} =
             BDS.Posts.create_post(%{
               project_id: project.id,
               title: "Delete Me",
               content: "Body",
               tags: ["Alpha", "Beta"]
             })

    assert {:ok, published_post} = BDS.Posts.publish_post(post.id)

    assert {:ok, :deleted} = BDS.Tags.delete_tag(doomed.id)

    reloaded_post = Repo.get!(Post, published_post.id)
    assert reloaded_post.tags == ["Beta"]
    assert Repo.get(BDS.Tags.Tag, doomed.id) == nil

    post_path = Path.join(temp_dir, reloaded_post.file_path)
    contents = File.read!(post_path)
    refute contents =~ "  - Alpha\n"
    assert contents =~ "tags:\n  - Beta\n"
    assert contents =~ "\n---\nBody\n"

    tags_path = Path.join([temp_dir, "meta", "tags.json"])
    assert [%{"name" => "Beta"}] = Jason.decode!(File.read!(tags_path))
  end

  test "sync_tags_from_posts creates missing tags from post tag arrays and refreshes tags.json",
       %{project: project, temp_dir: temp_dir} do
    assert {:ok, existing} =
             BDS.Tags.create_tag(%{
               project_id: project.id,
               name: "Existing",
               color: "#112233",
               post_template_slug: "feature-view"
             })

    assert {:ok, _post_a} =
             BDS.Posts.create_post(%{
               project_id: project.id,
               title: "First",
               content: "Body",
               tags: ["Existing", "Missing", "Missing"]
             })

    assert {:ok, _post_b} =
             BDS.Posts.create_post(%{
               project_id: project.id,
               title: "Second",
               content: "Body",
               tags: ["Another", "Missing"]
             })

    assert {:ok, synced_tags} = BDS.Tags.sync_tags_from_posts(project.id)

    assert Enum.map(synced_tags, & &1.name) == ["Another", "Existing", "Missing"]

    reloaded_existing = Repo.get!(BDS.Tags.Tag, existing.id)
    assert reloaded_existing.color == "#112233"
    assert reloaded_existing.post_template_slug == "feature-view"

    tags_path = Path.join([temp_dir, "meta", "tags.json"])

    assert [
             %{"name" => "Another"},
             %{
               "name" => "Existing",
               "color" => "#112233",
               "postTemplateSlug" => "feature-view"
             },
             %{"name" => "Missing"}
           ] = Jason.decode!(File.read!(tags_path))
  end

  test "colour_presets contains exactly 17 unique valid hex colours" do
    presets = BDS.Desktop.ShellLive.TagsEditor.colour_presets()
    assert length(presets) == 17
    assert length(Enum.uniq(presets)) == 17
    assert Enum.all?(presets, &Regex.match?(~r/^#[0-9a-fA-F]{6}$/, &1))
  end

  defp errors_on(changeset) do
    Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
      Regex.replace(~r"%{(\w+)}", message, fn _, key ->
        opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
      end)
    end)
  end
end
