defmodule Microwaveprop.BeaconsTest do
  use Microwaveprop.DataCase
  use ExUnitProperties

  import Microwaveprop.AccountsFixtures
  import Microwaveprop.BeaconsFixtures

  alias Microwaveprop.Beacons
  alias Microwaveprop.Beacons.Beacon

  @invalid_attrs %{
    frequency_mhz: nil,
    callsign: nil,
    grid: nil,
    lat: nil,
    lon: nil,
    power_mw: nil,
    height_ft: nil
  }

  describe "Beacon.format_freq/1" do
    test "renders integer frequencies with comma separators" do
      assert Beacon.format_freq(10_368) == "10,368"
    end

    test "renders whole-number floats cleanly (regression for 24192.0 crash)" do
      # 24192.0 used to crash format_freq because float rounding made
      # `frac == 0.0` sometimes false, and trim_trailing_zeros then stripped
      # the decimal point, leaving a single-element split result that
      # couldn't be destructured as `[_int, frac_part]`.
      assert Beacon.format_freq(24_192.0) == "24,192"
      assert Beacon.format_freq(10_368.0) == "10,368"
    end

    test "renders fractional frequencies with trimmed trailing zeros" do
      assert Beacon.format_freq(10_368.1) == "10,368.1"
      assert Beacon.format_freq(10_368.123) == "10,368.123"
    end

    test "handles nil" do
      assert Beacon.format_freq(nil) == ""
    end
  end

  describe "Beacon.format_mw/1" do
    test "handles whole-number floats cleanly (regression for the 24192 crash)" do
      assert Beacon.format_mw(100.0) == "100"
      assert Beacon.format_mw(1000.0) == "1,000"
    end

    test "renders integer power with commas" do
      assert Beacon.format_mw(5000) == "5,000"
    end

    test "renders fractional power with trimmed trailing zeros" do
      assert Beacon.format_mw(12.5) == "12.5"
      assert Beacon.format_mw(0.125) == "0.125"
    end

    test "handles nil" do
      assert Beacon.format_mw(nil) == ""
    end
  end

  describe "Beacon.format_freq/1 property tests" do
    # Ranges cover the realistic microwave band set (902 MHz to 241 GHz)
    # plus some float edge cases that trigger rounding quirks.
    property "never crashes on any integer in the microwave range" do
      check all(n <- integer(1..300_000)) do
        result = Beacon.format_freq(n)
        assert is_binary(result)
        assert result != ""
      end
    end

    property "never crashes on any float in the microwave range" do
      check all(n <- float(min: 1.0, max: 300_000.0)) do
        result = Beacon.format_freq(n)
        assert is_binary(result)
        assert result != ""
      end
    end

    property "never crashes on any float including whole numbers and edge floats" do
      check all(
              n <-
                one_of([
                  float(min: 0.0, max: 300_000.0),
                  map(integer(1..300_000), &(&1 * 1.0)),
                  constant(24_192.0),
                  constant(10_368.0),
                  constant(47_088.0)
                ])
            ) do
        assert is_binary(Beacon.format_freq(n))
      end
    end

    property "format_mw never crashes on any number" do
      check all(
              n <-
                one_of([
                  integer(0..1_000_000),
                  float(min: 0.0, max: 1_000_000.0),
                  map(integer(0..1000), &(&1 * 1.0))
                ])
            ) do
        assert is_binary(Beacon.format_mw(n))
      end
    end
  end

  describe "list_beacons/0" do
    test "returns only approved beacons" do
      u1 = user_fixture()
      u2 = user_fixture()
      {:ok, b1} = Beacons.create_beacon(u1, valid_beacon_attrs())
      {:ok, b1} = Beacons.approve_beacon(b1)
      _pending = beacon_fixture(u2, callsign: "W5TX")
      assert [^b1] = Beacons.list_beacons()
    end
  end

  describe "list_pending_beacons/0" do
    test "returns only unapproved beacons" do
      u1 = user_fixture()
      u2 = user_fixture()
      {:ok, approved} = Beacons.create_beacon(u1, valid_beacon_attrs())
      {:ok, _approved} = Beacons.approve_beacon(approved)
      pending = beacon_fixture(u2, callsign: "W5TX")
      assert [^pending] = Beacons.list_pending_beacons()
    end
  end

  describe "list_beacons_for_user/1" do
    test "returns both approved and pending beacons owned by the user" do
      user = user_fixture()
      other = user_fixture()

      {:ok, approved} = Beacons.create_beacon(user, valid_beacon_attrs())
      {:ok, approved} = Beacons.approve_beacon(approved)

      pending = beacon_fixture(user, callsign: "W5PND")
      _theirs = beacon_fixture(other, callsign: "W5THR")

      ids = user |> Beacons.list_beacons_for_user() |> Enum.map(& &1.id) |> Enum.sort()
      assert ids == Enum.sort([approved.id, pending.id])
    end

    test "returns an empty list when the user owns nothing" do
      user = user_fixture()
      assert Beacons.list_beacons_for_user(user) == []
    end
  end

  describe "approve_beacon/1" do
    test "marks the beacon as approved and broadcasts an update" do
      user = user_fixture()
      beacon = beacon_fixture(user)
      refute beacon.approved
      Beacons.subscribe_beacons()
      assert {:ok, approved} = Beacons.approve_beacon(beacon)
      assert approved.approved == true
      assert_receive {:updated, %Beacon{approved: true}}
    end
  end

  describe "get_beacon!/1" do
    test "returns the beacon with given id" do
      user = user_fixture()
      beacon = beacon_fixture(user)
      assert Beacons.get_beacon!(beacon.id).id == beacon.id
    end
  end

  describe "create_beacon/2" do
    test "with valid data creates a beacon and records the creator" do
      user = user_fixture()
      attrs = valid_beacon_attrs()

      assert {:ok, %Beacon{} = beacon} = Beacons.create_beacon(user, attrs)
      assert beacon.frequency_mhz == attrs.frequency_mhz
      assert beacon.callsign == "W5HN"
      assert beacon.lat == attrs.lat
      assert beacon.lon == attrs.lon
      assert beacon.power_mw == attrs.power_mw
      assert beacon.height_ft == attrs.height_ft
      assert beacon.user_id == user.id
    end

    test "anonymous submission creates a beacon with no user_id, unapproved" do
      attrs = valid_beacon_attrs()
      assert {:ok, %Beacon{} = beacon} = Beacons.create_beacon(nil, attrs)
      assert beacon.user_id == nil
      assert beacon.approved == false
    end

    test "new beacons default to unapproved" do
      user = user_fixture()
      assert {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs())
      assert beacon.approved == false
    end

    test "ignores client-supplied approved value" do
      user = user_fixture()
      attrs = Map.put(valid_beacon_attrs(), :approved, true)
      assert {:ok, beacon} = Beacons.create_beacon(user, attrs)
      assert beacon.approved == false
    end

    test "derives grid from lat/lon when grid is omitted" do
      user = user_fixture()
      # DFW is EM12
      {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(lat: 32.897, lon: -97.038))
      assert String.starts_with?(beacon.grid, "EM12")
    end

    test "derives lat/lon from grid when lat/lon are omitted" do
      user = user_fixture()

      attrs =
        [grid: "EM12kp"]
        |> valid_beacon_attrs()
        |> Map.drop([:lat, :lon])

      assert {:ok, beacon} = Beacons.create_beacon(user, attrs)
      assert beacon.grid == "EM12kp"
      assert_in_delta beacon.lat, 32.6458, 0.01
      assert_in_delta beacon.lon, -97.125, 0.01
    end

    test "keeps an explicitly provided valid grid" do
      user = user_fixture()
      {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(grid: "EM13"))
      assert beacon.grid == "EM13"
    end

    test "rejects an invalid grid" do
      user = user_fixture()

      assert {:error, changeset} =
               Beacons.create_beacon(user, valid_beacon_attrs(grid: "ZZ99"))

      assert "is not a valid Maidenhead grid" in errors_on(changeset).grid
    end

    test "defaults on_the_air to true" do
      user = user_fixture()
      {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs())
      assert beacon.on_the_air == true
    end

    test "accepts on_the_air: false" do
      user = user_fixture()
      {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(on_the_air: false))
      assert beacon.on_the_air == false
    end

    test "defaults keying to on_off" do
      user = user_fixture()
      {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs())
      assert beacon.keying == "on_off"
    end

    test "accepts keying: fsk" do
      user = user_fixture()
      {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(keying: "fsk"))
      assert beacon.keying == "fsk"
    end

    test "rejects an unknown keying" do
      user = user_fixture()

      assert {:error, changeset} =
               Beacons.create_beacon(user, valid_beacon_attrs(keying: "cw"))

      assert "is invalid" in errors_on(changeset).keying
    end

    test "accepts fm_voice keying" do
      user = user_fixture()
      {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(keying: "fm_voice"))
      assert beacon.keying == "fm_voice"
    end

    test "accepts wspr keying" do
      user = user_fixture()
      {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(keying: "wspr"))
      assert beacon.keying == "wspr"
    end

    test "accepts all Q65 keying variants" do
      user = user_fixture()

      variants =
        for letter <- ~w(a b c d e), period <- ~w(15 30 60 120), do: "q65#{letter}_#{period}"

      for keying <- variants do
        assert {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(keying: keying))
        assert beacon.keying == keying
      end
    end

    test "defaults bearing to omni" do
      user = user_fixture()
      {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs())
      assert beacon.bearing == "omni"
    end

    test "normalizes empty bearing to omni" do
      user = user_fixture()
      {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(bearing: ""))
      assert beacon.bearing == "omni"
    end

    test "normalizes 'Omni' to 'omni'" do
      user = user_fixture()
      {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(bearing: "Omni"))
      assert beacon.bearing == "omni"
    end

    test "accepts a numeric bearing" do
      user = user_fixture()
      {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(bearing: "90"))
      assert beacon.bearing == "90"
    end

    test "accepts bearing of 0 and 360" do
      user = user_fixture()
      {:ok, b0} = Beacons.create_beacon(user, valid_beacon_attrs(bearing: "0"))
      assert b0.bearing == "0"
      {:ok, b360} = Beacons.create_beacon(user, valid_beacon_attrs(bearing: "360"))
      assert b360.bearing == "360"
    end

    test "rejects a non-numeric non-omni bearing" do
      user = user_fixture()

      assert {:error, changeset} =
               Beacons.create_beacon(user, valid_beacon_attrs(bearing: "north"))

      assert errors_on(changeset).bearing != []
    end

    test "rejects a bearing greater than 360" do
      user = user_fixture()

      assert {:error, changeset} =
               Beacons.create_beacon(user, valid_beacon_attrs(bearing: "400"))

      assert errors_on(changeset).bearing != []
    end

    test "rejects a negative bearing" do
      user = user_fixture()

      assert {:error, changeset} =
               Beacons.create_beacon(user, valid_beacon_attrs(bearing: "-10"))

      assert errors_on(changeset).bearing != []
    end

    test "stores beamwidth_deg" do
      user = user_fixture()
      {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(beamwidth_deg: 30.0))
      assert beacon.beamwidth_deg == 30.0
    end

    test "rejects a non-positive beamwidth" do
      user = user_fixture()

      assert {:error, changeset} =
               Beacons.create_beacon(user, valid_beacon_attrs(beamwidth_deg: 0))

      assert errors_on(changeset).beamwidth_deg != []
    end

    test "rejects beamwidth > 360" do
      user = user_fixture()

      assert {:error, changeset} =
               Beacons.create_beacon(user, valid_beacon_attrs(beamwidth_deg: 400))

      assert errors_on(changeset).beamwidth_deg != []
    end

    test "stores notes" do
      user = user_fixture()
      notes = "Antenna 20 ft AGL, horizontal polarization"
      {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(notes: notes))
      assert beacon.notes == notes
    end

    test "upcases the callsign" do
      user = user_fixture()
      {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(callsign: "w5hn"))
      assert beacon.callsign == "W5HN"
    end

    test "rejects non-positive frequency" do
      user = user_fixture()

      assert {:error, changeset} =
               Beacons.create_beacon(user, valid_beacon_attrs(frequency_mhz: 0))

      assert "must be greater than 0" in errors_on(changeset).frequency_mhz
    end

    test "with invalid data returns error changeset" do
      user = user_fixture()
      assert {:error, %Ecto.Changeset{}} = Beacons.create_beacon(user, @invalid_attrs)
    end
  end

  describe "update_beacon/2" do
    test "with valid data updates the beacon" do
      user = user_fixture()
      beacon = beacon_fixture(user)

      update_attrs = %{frequency_mhz: 24_192.1, power_mw: 5.0}

      assert {:ok, %Beacon{} = beacon} = Beacons.update_beacon(beacon, update_attrs)
      assert beacon.frequency_mhz == 24_192.1
      assert beacon.power_mw == 5.0
    end

    test "with invalid data returns error changeset" do
      user = user_fixture()
      beacon = beacon_fixture(user)
      assert {:error, %Ecto.Changeset{}} = Beacons.update_beacon(beacon, @invalid_attrs)
      assert Beacons.get_beacon!(beacon.id).callsign == beacon.callsign
    end
  end

  describe "delete_beacon/1" do
    test "deletes the beacon" do
      user = user_fixture()
      beacon = beacon_fixture(user)
      assert {:ok, %Beacon{}} = Beacons.delete_beacon(beacon)
      assert_raise Ecto.NoResultsError, fn -> Beacons.get_beacon!(beacon.id) end
    end
  end

  describe "change_beacon/1" do
    test "returns a beacon changeset" do
      user = user_fixture()
      beacon = beacon_fixture(user)
      assert %Ecto.Changeset{} = Beacons.change_beacon(beacon)
    end
  end
end
