defmodule Microwaveprop.Radio.ContactSubmissionTest do
  use Microwaveprop.DataCase, async: true
  use ExUnitProperties

  alias Microwaveprop.Radio.Contact

  @valid_attrs %{
    station1: "W5XD",
    station2: "K5TR",
    qso_timestamp: ~U[2026-03-28 18:00:00Z],
    mode: "CW",
    band: "1296",
    grid1: "EM12",
    grid2: "EM00",
    submitter_email: "test@example.com"
  }

  describe "submission_changeset/2" do
    test "valid attrs produce a valid changeset" do
      changeset = Contact.submission_changeset(%Contact{}, @valid_attrs)
      assert changeset.valid?
    end

    test "requires station1, station2, qso_timestamp, band, grid1, grid2, submitter_email" do
      changeset = Contact.submission_changeset(%Contact{}, %{})
      errors = errors_on(changeset)

      assert errors[:station1]
      assert errors[:station2]
      assert errors[:qso_timestamp]
      assert errors[:band]
      assert errors[:grid1]
      assert errors[:grid2]
      assert errors[:submitter_email]

      # Mode is optional on submission
      refute errors[:mode]
    end

    test "accepts a blank mode and normalises it to nil" do
      for blank <- ["", "   ", nil] do
        attrs = Map.put(@valid_attrs, :mode, blank)
        changeset = Contact.submission_changeset(%Contact{}, attrs)
        assert changeset.valid?, "Expected blank mode #{inspect(blank)} to validate"
        assert Ecto.Changeset.get_field(changeset, :mode) == nil
      end
    end

    test "rejects invalid grid1" do
      attrs = Map.put(@valid_attrs, :grid1, "ZZ99")
      changeset = Contact.submission_changeset(%Contact{}, attrs)
      assert "is not a valid Maidenhead grid square" in errors_on(changeset).grid1
    end

    test "rejects invalid grid2" do
      attrs = Map.put(@valid_attrs, :grid2, "ZZ99")
      changeset = Contact.submission_changeset(%Contact{}, attrs)
      assert "is not a valid Maidenhead grid square" in errors_on(changeset).grid2
    end

    test "accepts valid 6-char grids" do
      attrs = %{@valid_attrs | grid1: "EM12ab", grid2: "EM00cd"}
      changeset = Contact.submission_changeset(%Contact{}, attrs)
      assert changeset.valid?
    end

    test "rejects invalid mode" do
      attrs = Map.put(@valid_attrs, :mode, "RTTY")
      changeset = Contact.submission_changeset(%Contact{}, attrs)
      assert errors_on(changeset).mode
    end

    test "accepts valid modes" do
      for mode <- ~w(CW SSB FM FT8 FT4 Q65) do
        attrs = Map.put(@valid_attrs, :mode, mode)
        changeset = Contact.submission_changeset(%Contact{}, attrs)
        assert changeset.valid?, "Expected mode #{mode} to be valid"
      end
    end

    test "rejects invalid band" do
      attrs = Map.put(@valid_attrs, :band, "28")
      changeset = Contact.submission_changeset(%Contact{}, attrs)
      assert errors_on(changeset).band
    end

    test "accepts valid bands" do
      for band <- ~w(50 144 222 432 902 1296 2304 3400 5760 10000 24000 47000 75000) do
        attrs = Map.put(@valid_attrs, :band, band)
        changeset = Contact.submission_changeset(%Contact{}, attrs)
        assert changeset.valid?, "Expected band #{band} to be valid"
      end
    end

    test "rejects email without @" do
      attrs = Map.put(@valid_attrs, :submitter_email, "notanemail")
      changeset = Contact.submission_changeset(%Contact{}, attrs)
      assert errors_on(changeset).submitter_email
    end

    test "accepts private flag and defaults to false" do
      changeset = Contact.submission_changeset(%Contact{}, @valid_attrs)
      assert changeset.valid?
      assert Ecto.Changeset.get_field(changeset, :private) == false

      attrs = Map.put(@valid_attrs, :private, true)
      changeset = Contact.submission_changeset(%Contact{}, attrs)
      assert changeset.valid?
      assert Ecto.Changeset.get_field(changeset, :private) == true
    end

    test "does not cast user_submitted" do
      attrs = Map.put(@valid_attrs, :user_submitted, true)
      changeset = Contact.submission_changeset(%Contact{}, attrs)
      refute Ecto.Changeset.get_change(changeset, :user_submitted)
    end

    test "accepts antenna heights within [0, 1000] ft" do
      for {h1, h2} <- [{0, 0}, {10, 10}, {500, 750}, {999, 1000}] do
        attrs = Map.merge(@valid_attrs, %{height1_ft: h1, height2_ft: h2})
        changeset = Contact.submission_changeset(%Contact{}, attrs)
        assert changeset.valid?, "expected heights h1=#{h1} h2=#{h2} to validate"
      end
    end

    test "rejects negative antenna heights" do
      attrs = Map.put(@valid_attrs, :height1_ft, -5)
      changeset = Contact.submission_changeset(%Contact{}, attrs)
      assert errors_on(changeset)[:height1_ft]
    end

    test "rejects antenna heights above 1000 ft" do
      attrs = Map.put(@valid_attrs, :height2_ft, 1001)
      changeset = Contact.submission_changeset(%Contact{}, attrs)
      assert errors_on(changeset)[:height2_ft]
    end

    test "collapses a whitespace-only mode to no-mode (not an invalid-mode error)" do
      for blank <- ["", "   ", "\t", "\n "] do
        attrs = Map.put(@valid_attrs, :mode, blank)
        changeset = Contact.submission_changeset(%Contact{}, attrs)

        assert changeset.valid?, "expected blank mode #{inspect(blank)} to validate"
        refute errors_on(changeset)[:mode]
      end
    end

    property "accepts every sanctioned band" do
      check all(
              band <-
                StreamData.member_of(
                  ~w(50 144 222 432 902 1296 2304 3400 5760 10000 24000 47000 68000 75000 122000 134000 241000)
                )
            ) do
        attrs = Map.put(@valid_attrs, :band, band)
        changeset = Contact.submission_changeset(%Contact{}, attrs)
        assert changeset.valid?
      end
    end

    property "rejects any band string outside the sanctioned list" do
      check all(
              band <- string(:alphanumeric, min_length: 1, max_length: 10),
              band not in ~w(50 144 222 432 902 1296 2304 3400 5760 10000 24000 47000 68000 75000 122000 134000 241000)
            ) do
        attrs = Map.put(@valid_attrs, :band, band)
        changeset = Contact.submission_changeset(%Contact{}, attrs)
        refute changeset.valid?
        assert errors_on(changeset)[:band]
      end
    end
  end

  describe "notes field" do
    test "accepts a notes string" do
      attrs = Map.put(@valid_attrs, :notes, "Rare opening via sporadic-E along the TX/OK front")
      changeset = Contact.submission_changeset(%Contact{}, attrs)
      assert changeset.valid?
      assert Ecto.Changeset.get_change(changeset, :notes) =~ "sporadic-E"
    end

    test "trims whitespace and treats blank notes as nil" do
      for blank <- ["", "   ", "\n\t "] do
        attrs = Map.put(@valid_attrs, :notes, blank)
        changeset = Contact.submission_changeset(%Contact{}, attrs)
        assert changeset.valid?, "expected blank notes #{inspect(blank)} to validate"
        refute Ecto.Changeset.get_change(changeset, :notes) in [blank]
      end
    end

    test "rejects notes longer than 2000 characters" do
      attrs = Map.put(@valid_attrs, :notes, String.duplicate("x", 2001))
      changeset = Contact.submission_changeset(%Contact{}, attrs)
      refute changeset.valid?
      assert errors_on(changeset)[:notes]
    end

    property "accepts any non-blank string up to 2000 graphemes" do
      check all(
              len <- integer(1..2000),
              notes <- string(:printable, length: len),
              String.trim(notes) != ""
            ) do
        attrs = Map.put(@valid_attrs, :notes, notes)
        changeset = Contact.submission_changeset(%Contact{}, attrs)

        assert changeset.valid?,
               "expected #{len}-char notes to validate, got errors: " <>
                 inspect(errors_on(changeset))
      end
    end

    property "rejects any notes string longer than 2000 characters" do
      check all(
              overage <- integer(1..500),
              notes <- string(:alphanumeric, length: 2000 + overage)
            ) do
        attrs = Map.put(@valid_attrs, :notes, notes)
        changeset = Contact.submission_changeset(%Contact{}, attrs)

        refute changeset.valid?,
               "expected #{String.length(notes)}-char notes to be rejected"

        assert errors_on(changeset)[:notes]
      end
    end
  end

  describe "create_contact/1 deduplication" do
    alias Microwaveprop.Radio

    test "rejects duplicate and returns the existing contact" do
      assert {:ok, original} = Radio.create_contact(@valid_attrs)

      assert {:error, :duplicate, ^original} = Radio.create_contact(@valid_attrs)
    end

    test "allows same stations on a different band" do
      assert {:ok, _} = Radio.create_contact(@valid_attrs)
      assert {:ok, _} = Radio.create_contact(%{@valid_attrs | band: "2304"})
    end

    test "allows same stations with different grids" do
      assert {:ok, _} = Radio.create_contact(@valid_attrs)
      assert {:ok, _} = Radio.create_contact(%{@valid_attrs | grid1: "EM13"})
    end

    test "allows contact outside the 1-hour window" do
      assert {:ok, _} = Radio.create_contact(@valid_attrs)

      later = DateTime.add(@valid_attrs.qso_timestamp, 3601, :second)
      assert {:ok, _} = Radio.create_contact(%{@valid_attrs | qso_timestamp: later})
    end

    test "detects duplicates regardless of station order" do
      assert {:ok, original} = Radio.create_contact(@valid_attrs)

      swapped = %{@valid_attrs | station1: "K5TR", grid1: "EM00", station2: "W5XD", grid2: "EM12"}
      assert {:error, :duplicate, ^original} = Radio.create_contact(swapped)
    end
  end
end
