defmodule Microwaveprop.Radio.ContactEditTest do
  use Microwaveprop.DataCase, async: true

  alias Microwaveprop.Accounts
  alias Microwaveprop.Radio
  alias Microwaveprop.Radio.Contact
  alias Microwaveprop.Radio.ContactEdit
  alias Microwaveprop.Terrain.ElevationClient
  alias Microwaveprop.Weather.HrrrClient
  alias Microwaveprop.Weather.IemClient

  setup do
    # Height edits auto-enqueue TerrainProfileWorker (which Oban runs
    # inline in tests). Stub the external data sources so the job can
    # complete without hitting the network.
    Req.Test.stub(ElevationClient, fn conn ->
      params = Plug.Conn.fetch_query_params(conn).query_params
      lat_count = params["latitude"] |> String.split(",") |> length()
      Req.Test.json(conn, %{"elevation" => List.duplicate(200.0, lat_count)})
    end)

    Req.Test.stub(HrrrClient, fn conn ->
      Plug.Conn.send_resp(conn, 404, "not found")
    end)

    Req.Test.stub(IemClient, fn conn ->
      Plug.Conn.send_resp(conn, 404, "not found")
    end)

    :ok
  end

  @contact_attrs %{
    station1: "W5ISP",
    station2: "K5TR",
    qso_timestamp: ~U[2026-04-01 14:00:00Z],
    grid1: "EM13",
    grid2: "EL29",
    pos1: %{"lat" => 33.5, "lon" => -97.0},
    pos2: %{"lat" => 29.5, "lon" => -98.5},
    mode: "CW",
    band: Decimal.new("10000"),
    distance_km: Decimal.new("450")
  }

  @user_attrs %{
    callsign: "W5TEST",
    name: "Test User",
    email: "test@example.com",
    password: "testpassword123"
  }

  @admin_attrs %{
    callsign: "N5ADM",
    name: "Admin User",
    email: "admin@example.com",
    password: "adminpassword123"
  }

  defp create_contact(_) do
    {:ok, contact} = %Contact{} |> Contact.changeset(@contact_attrs) |> Repo.insert()
    %{contact: contact}
  end

  defp create_user(_) do
    {:ok, user} = Accounts.register_user(@user_attrs)
    %{user: user}
  end

  defp create_admin(_) do
    {:ok, admin} = Accounts.register_user(@admin_attrs)
    admin = admin |> Ecto.Changeset.change(is_admin: true) |> Repo.update!()
    %{admin: admin}
  end

  describe "ContactEdit.changeset/2" do
    setup [:create_contact, :create_user]

    test "valid with proper proposed changes", %{contact: contact, user: user} do
      changeset =
        ContactEdit.changeset(%ContactEdit{}, %{
          contact_id: contact.id,
          user_id: user.id,
          proposed_changes: %{"grid1" => "EM13kk"}
        })

      assert changeset.valid?
    end

    test "rejects empty proposed_changes", %{contact: contact, user: user} do
      changeset =
        ContactEdit.changeset(%ContactEdit{}, %{
          contact_id: contact.id,
          user_id: user.id,
          proposed_changes: %{}
        })

      refute changeset.valid?
      assert "must contain at least one change" in errors_on(changeset).proposed_changes
    end

    test "rejects invalid fields in proposed_changes", %{contact: contact, user: user} do
      changeset =
        ContactEdit.changeset(%ContactEdit{}, %{
          contact_id: contact.id,
          user_id: user.id,
          proposed_changes: %{"distance_km" => 100}
        })

      refute changeset.valid?
      assert Enum.any?(errors_on(changeset).proposed_changes, &String.contains?(&1, "invalid fields"))
    end

    test "validates callsign format", %{contact: contact, user: user} do
      changeset =
        ContactEdit.changeset(%ContactEdit{}, %{
          contact_id: contact.id,
          user_id: user.id,
          proposed_changes: %{"station1" => "bad call!"}
        })

      refute changeset.valid?
    end

    test "validates grid format", %{contact: contact, user: user} do
      changeset =
        ContactEdit.changeset(%ContactEdit{}, %{
          contact_id: contact.id,
          user_id: user.id,
          proposed_changes: %{"grid1" => "ZZZZZZ"}
        })

      refute changeset.valid?
    end

    test "validates band value", %{contact: contact, user: user} do
      changeset =
        ContactEdit.changeset(%ContactEdit{}, %{
          contact_id: contact.id,
          user_id: user.id,
          proposed_changes: %{"band" => 99_999}
        })

      refute changeset.valid?
    end

    test "accepts integer height1_ft and height2_ft", %{contact: contact, user: user} do
      changeset =
        ContactEdit.changeset(%ContactEdit{}, %{
          contact_id: contact.id,
          user_id: user.id,
          proposed_changes: %{"height1_ft" => 25, "height2_ft" => 60}
        })

      assert changeset.valid?
    end

    test "rejects non-integer height1_ft", %{contact: contact, user: user} do
      changeset =
        ContactEdit.changeset(%ContactEdit{}, %{
          contact_id: contact.id,
          user_id: user.id,
          proposed_changes: %{"height1_ft" => "tall"}
        })

      refute changeset.valid?
    end

    test "rejects out-of-range height2_ft", %{contact: contact, user: user} do
      changeset =
        ContactEdit.changeset(%ContactEdit{}, %{
          contact_id: contact.id,
          user_id: user.id,
          proposed_changes: %{"height2_ft" => 9999}
        })

      refute changeset.valid?
    end

    test "accepts boolean private flag", %{contact: contact, user: user} do
      changeset =
        ContactEdit.changeset(%ContactEdit{}, %{
          contact_id: contact.id,
          user_id: user.id,
          proposed_changes: %{"private" => true}
        })

      assert changeset.valid?
    end

    test "rejects non-boolean private", %{contact: contact, user: user} do
      changeset =
        ContactEdit.changeset(%ContactEdit{}, %{
          contact_id: contact.id,
          user_id: user.id,
          proposed_changes: %{"private" => "yes"}
        })

      refute changeset.valid?
    end

    test "validates mode value", %{contact: contact, user: user} do
      changeset =
        ContactEdit.changeset(%ContactEdit{}, %{
          contact_id: contact.id,
          user_id: user.id,
          proposed_changes: %{"mode" => "INVALID"}
        })

      refute changeset.valid?
    end

    test "requires contact_id, user_id, proposed_changes" do
      changeset = ContactEdit.changeset(%ContactEdit{}, %{})

      assert %{
               contact_id: ["can't be blank"],
               user_id: ["can't be blank"],
               proposed_changes: ["can't be blank"]
             } = errors_on(changeset)
    end
  end

  describe "Radio.create_contact_edit/3" do
    setup [:create_contact, :create_user]

    test "creates a pending edit with only changed fields", %{contact: contact, user: user} do
      proposed = %{"grid1" => "EM13kk", "station1" => "W5ISP"}

      assert {:ok, edit} = Radio.create_contact_edit(contact, user, proposed)
      assert edit.status == :pending
      assert edit.proposed_changes == %{"grid1" => "EM13KK"}
      assert edit.contact_id == contact.id
      assert edit.user_id == user.id
    end

    test "rejects when nothing actually changed", %{contact: contact, user: user} do
      proposed = %{"station1" => "W5ISP", "mode" => "CW"}

      assert {:error, changeset} = Radio.create_contact_edit(contact, user, proposed)
      assert "must contain at least one change" in errors_on(changeset).proposed_changes
    end
  end

  describe "Radio.list_pending_edits/0" do
    setup [:create_contact, :create_user]

    test "returns pending edits", %{contact: contact, user: user} do
      {:ok, _e1} =
        Radio.create_contact_edit(contact, user, %{"grid1" => "EM13kk"})

      {:ok, _e2} =
        Radio.create_contact_edit(contact, user, %{"grid2" => "EL29ab"})

      edits = Radio.list_pending_edits()
      assert length(edits) == 2

      fields = Enum.flat_map(edits, &Map.keys(&1.proposed_changes))
      assert "grid1" in fields
      assert "grid2" in fields
    end
  end

  describe "Radio.approve_edit/3" do
    setup [:create_contact, :create_user, :create_admin]

    test "applies changes to the contact and marks approved", %{
      contact: contact,
      user: user,
      admin: admin
    } do
      {:ok, edit} = Radio.create_contact_edit(contact, user, %{"grid1" => "EM13kk"})

      assert {:ok, approved_edit} = Radio.approve_edit(edit, admin, "Confirmed via LOTW")
      assert approved_edit.status == :approved
      assert approved_edit.admin_note == "Confirmed via LOTW"
      assert approved_edit.reviewed_by_id == admin.id
      assert approved_edit.reviewed_at

      updated_contact = Radio.get_contact!(contact.id)
      assert updated_contact.grid1 == "EM13KK"
    end

    test "re-enqueues enrichment when grid changes", %{
      contact: contact,
      user: user,
      admin: admin
    } do
      {:ok, edit} = Radio.create_contact_edit(contact, user, %{"grid1" => "EM15ab"})
      {:ok, _approved} = Radio.approve_edit(edit, admin, nil)

      updated = Radio.get_contact!(contact.id)
      assert updated.grid1 == "EM15AB"
      assert updated.pos1 != contact.pos1
    end
  end

  describe "Radio.reject_edit/3" do
    setup [:create_contact, :create_user, :create_admin]

    test "marks edit as rejected without changing contact", %{
      contact: contact,
      user: user,
      admin: admin
    } do
      {:ok, edit} = Radio.create_contact_edit(contact, user, %{"grid1" => "EM13kk"})

      assert {:ok, rejected_edit} = Radio.reject_edit(edit, admin, "Original data correct")
      assert rejected_edit.status == :rejected
      assert rejected_edit.admin_note == "Original data correct"

      unchanged_contact = Radio.get_contact!(contact.id)
      assert unchanged_contact.grid1 == contact.grid1
    end
  end

  describe "Radio.pending_edit_count/0" do
    setup [:create_contact, :create_user]

    test "counts only pending edits", %{contact: contact, user: user} do
      {:ok, _} = Radio.create_contact_edit(contact, user, %{"grid1" => "EM13kk"})
      {:ok, _} = Radio.create_contact_edit(contact, user, %{"grid2" => "EL29ab"})

      assert Radio.pending_edit_count() == 2
    end
  end

  describe "Radio.apply_owner_edit/3" do
    setup [:create_user, :create_admin]

    defp owned_contact(user) do
      {:ok, contact} =
        %Contact{}
        |> Contact.changeset(Map.put(@contact_attrs, :user_id, user.id))
        |> Microwaveprop.Repo.insert()

      contact
    end

    test "owner can apply edits directly without going through review", %{user: user} do
      contact = owned_contact(user)

      assert {:ok, updated} =
               Radio.apply_owner_edit(contact, user, %{"grid1" => "EM15ab"})

      assert updated.grid1 == "EM15AB"
      # No pending edits were created
      assert Radio.pending_edit_for_user(contact.id, user.id) == nil
    end

    test "re-enqueues enrichment when owner changes a grid", %{user: user} do
      contact = owned_contact(user)

      {:ok, updated} = Radio.apply_owner_edit(contact, user, %{"grid1" => "EM15ab"})

      assert updated.pos1 != contact.pos1
      assert updated.hrrr_status == :pending
      assert updated.terrain_status == :pending
    end

    test "returns :not_owner error when user did not submit the contact", %{user: user} do
      {:ok, other} = Accounts.register_user(%{@user_attrs | email: "other@example.com", callsign: "W5OTH"})
      contact = owned_contact(other)

      assert {:error, :not_owner} = Radio.apply_owner_edit(contact, user, %{"grid1" => "EM15ab"})

      unchanged = Radio.get_contact!(contact.id)
      assert unchanged.grid1 == contact.grid1
    end

    test "returns :not_owner on anonymous contacts (user_id is nil)", %{user: user} do
      {:ok, anon} = %Contact{} |> Contact.changeset(@contact_attrs) |> Microwaveprop.Repo.insert()

      assert {:error, :not_owner} = Radio.apply_owner_edit(anon, user, %{"grid1" => "EM15ab"})
    end

    test "returns error tuple when no changes are actually proposed", %{user: user} do
      contact = owned_contact(user)

      assert {:error, :no_changes} =
               Radio.apply_owner_edit(contact, user, %{"station1" => contact.station1})
    end

    test "owner can set height1_ft and height2_ft", %{user: user} do
      contact = owned_contact(user)

      assert {:ok, updated} =
               Radio.apply_owner_edit(contact, user, %{"height1_ft" => 20, "height2_ft" => 75})

      assert updated.height1_ft == 20
      assert updated.height2_ft == 75
    end

    test "height change resets terrain_status so terrain is re-analyzed", %{user: user} do
      contact = owned_contact(user)
      contact = contact |> Ecto.Changeset.change(terrain_status: :complete) |> Repo.update!()

      {:ok, updated} = Radio.apply_owner_edit(contact, user, %{"height1_ft" => 30})

      assert updated.height1_ft == 30
      assert updated.terrain_status == :pending
    end
  end

  describe "Radio.owner?/2" do
    setup [:create_user]

    test "true when contact.user_id matches the user", %{user: user} do
      {:ok, contact} =
        %Contact{}
        |> Contact.changeset(Map.put(@contact_attrs, :user_id, user.id))
        |> Microwaveprop.Repo.insert()

      assert Radio.owner?(contact, user)
    end

    test "false when contact.user_id is nil", %{user: user} do
      {:ok, contact} = %Contact{} |> Contact.changeset(@contact_attrs) |> Microwaveprop.Repo.insert()
      refute Radio.owner?(contact, user)
    end

    test "false when user is nil" do
      {:ok, contact} = %Contact{} |> Contact.changeset(@contact_attrs) |> Microwaveprop.Repo.insert()
      refute Radio.owner?(contact, nil)
    end
  end
end
