defmodule Microwaveprop.Radio.ImportMatcherTest do
  use ExUnit.Case, async: true

  alias Microwaveprop.Radio.Contact
  alias Microwaveprop.Radio.ImportMatcher

  defp build_contact(attrs \\ %{}) do
    defaults = %{
      id: Ecto.UUID.generate(),
      station1: "W5XD",
      station2: "K5TR",
      grid1: "EM12",
      grid2: "EM00",
      band: Decimal.new("10000"),
      mode: "CW",
      qso_timestamp: ~U[2026-03-28 18:00:00Z]
    }

    struct(Contact, Map.merge(defaults, attrs))
  end

  defp upload(attrs) do
    defaults = %{
      "station1" => "W5XD",
      "station2" => "K5TR",
      "grid1" => "EM12",
      "grid2" => "EM00",
      "band" => "10000",
      "mode" => "CW",
      "qso_timestamp" => "2026-03-28T18:00:00Z"
    }

    Map.merge(defaults, attrs)
  end

  describe "prefix_key/5" do
    test "is direction-agnostic" do
      a = ImportMatcher.prefix_key("W5XD", "EM12kp", "K5TR", "EM00cd", 10_000)
      b = ImportMatcher.prefix_key("K5TR", "EM00cd", "W5XD", "EM12kp", 10_000)
      assert a == b
    end

    test "uppercases and trims callsigns and grids" do
      a = ImportMatcher.prefix_key(" w5xd ", " em12kp ", " k5tr ", " em00 ", 10_000)
      b = ImportMatcher.prefix_key("W5XD", "EM12KP", "K5TR", "EM00", 10_000)
      assert a == b
    end

    test "uses only first 4 characters of the grid" do
      with_precision = ImportMatcher.prefix_key("W5XD", "EM12kp", "K5TR", "EM00cd", 10_000)
      just_four = ImportMatcher.prefix_key("W5XD", "EM12", "K5TR", "EM00", 10_000)
      assert with_precision == just_four
    end

    test "different band produces different key" do
      a = ImportMatcher.prefix_key("W5XD", "EM12", "K5TR", "EM00", 10_000)
      b = ImportMatcher.prefix_key("W5XD", "EM12", "K5TR", "EM00", 24_000)
      refute a == b
    end

    test "invalid when any grid is under 4 chars" do
      assert ImportMatcher.prefix_key("W5XD", "EM1", "K5TR", "EM00", 10_000) == :invalid
      assert ImportMatcher.prefix_key("W5XD", "EM12", "K5TR", "EM", 10_000) == :invalid
      assert ImportMatcher.prefix_key("W5XD", nil, "K5TR", "EM00", 10_000) == :invalid
      assert ImportMatcher.prefix_key("W5XD", "EM12", "K5TR", nil, 10_000) == :invalid
    end

    test "band accepted as integer, string, or Decimal" do
      int_key = ImportMatcher.prefix_key("W5XD", "EM12", "K5TR", "EM00", 10_000)
      str_key = ImportMatcher.prefix_key("W5XD", "EM12", "K5TR", "EM00", "10000")
      dec_key = ImportMatcher.prefix_key("W5XD", "EM12", "K5TR", "EM00", Decimal.new("10000"))
      assert int_key == str_key
      assert int_key == dec_key
    end
  end

  describe "classify_against_existing/2" do
    test "identical grids and mode classify as duplicate" do
      existing = build_contact()
      up = upload(%{})
      assert ImportMatcher.classify_against_existing(up, existing) == :duplicate
    end

    test "upload extends grid1 -> refinement" do
      existing = build_contact()
      up = upload(%{"grid1" => "EM12kp37"})

      assert ImportMatcher.classify_against_existing(up, existing) ==
               {:refinement, %{grid1: "EM12KP37"}}
    end

    test "upload extends grid2 -> refinement" do
      existing = build_contact()
      up = upload(%{"grid2" => "EM00cd"})

      assert ImportMatcher.classify_against_existing(up, existing) ==
               {:refinement, %{grid2: "EM00CD"}}
    end

    test "upload extends both grids -> refinement with both changes" do
      existing = build_contact()
      up = upload(%{"grid1" => "EM12kp", "grid2" => "EM00cd"})

      assert ImportMatcher.classify_against_existing(up, existing) ==
               {:refinement, %{grid1: "EM12KP", grid2: "EM00CD"}}
    end

    test "direction-swapped upload maps changes into existing's orientation" do
      existing = build_contact(%{station1: "W5XD", grid1: "EM12", station2: "K5TR", grid2: "EM00"})

      up =
        upload(%{
          "station1" => "K5TR",
          "grid1" => "EM00bb",
          "station2" => "W5XD",
          "grid2" => "EM12aa"
        })

      assert ImportMatcher.classify_against_existing(up, existing) ==
               {:refinement, %{grid1: "EM12AA", grid2: "EM00BB"}}
    end

    test "shorter-than-existing upload grid -> duplicate (not refinement)" do
      existing = build_contact(%{grid1: "EM12kp"})
      up = upload(%{"grid1" => "EM12"})
      assert ImportMatcher.classify_against_existing(up, existing) == :duplicate
    end

    test "same-length but different grid -> contradiction" do
      existing = build_contact(%{grid1: "EM12"})
      up = upload(%{"grid1" => "EM13"})
      assert ImportMatcher.classify_against_existing(up, existing) == :contradiction
    end

    test "existing mode nil + upload mode CW -> mode refinement" do
      existing = build_contact(%{mode: nil})
      up = upload(%{"mode" => "CW"})

      assert ImportMatcher.classify_against_existing(up, existing) ==
               {:refinement, %{mode: "CW"}}
    end

    test "existing mode CW + upload mode SSB -> contradiction" do
      existing = build_contact(%{mode: "CW"})
      up = upload(%{"mode" => "SSB"})
      assert ImportMatcher.classify_against_existing(up, existing) == :contradiction
    end

    test "existing mode CW + upload mode nil -> duplicate (no change)" do
      existing = build_contact(%{mode: "CW"})
      up = upload(%{"mode" => nil})
      assert ImportMatcher.classify_against_existing(up, existing) == :duplicate
    end

    test "both modes nil -> duplicate" do
      existing = build_contact(%{mode: nil})
      up = upload(%{"mode" => nil})
      assert ImportMatcher.classify_against_existing(up, existing) == :duplicate
    end

    test "same callsign on both sides of upload -> duplicate (ambiguous)" do
      existing = build_contact(%{station1: "W5XD", station2: "K5TR"})
      up = upload(%{"station1" => "W5XD", "station2" => "W5XD", "grid1" => "EM12kp"})
      assert ImportMatcher.classify_against_existing(up, existing) == :duplicate
    end

    test "grid refinement + mode refinement combined returns both keys" do
      existing = build_contact(%{grid1: "EM12", mode: nil})
      up = upload(%{"grid1" => "EM12kp", "mode" => "CW"})

      assert ImportMatcher.classify_against_existing(up, existing) ==
               {:refinement, %{grid1: "EM12KP", mode: "CW"}}
    end

    test "grid contradiction on one side trumps refinement on other" do
      existing = build_contact(%{grid1: "EM12", grid2: "EM00"})
      up = upload(%{"grid1" => "EM13", "grid2" => "EM00cd"})
      assert ImportMatcher.classify_against_existing(up, existing) == :contradiction
    end

    test "refinement values are uppercased and trimmed" do
      existing = build_contact()
      up = upload(%{"grid1" => "  em12kp  "})

      assert ImportMatcher.classify_against_existing(up, existing) ==
               {:refinement, %{grid1: "EM12KP"}}
    end
  end
end
