defmodule MicrowavepropWeb.BeaconLiveTest do
  use MicrowavepropWeb.ConnCase
  use ExUnitProperties

  import Microwaveprop.AccountsFixtures
  import Microwaveprop.BeaconsFixtures
  import Phoenix.LiveViewTest

  alias Microwaveprop.Accounts
  alias Microwaveprop.Beacons

  defp promote_to_admin(user) do
    {:ok, user} = Accounts.admin_update_user(user, %{is_admin: true})
    user
  end

  defp admin_user_fixture do
    promote_to_admin(user_fixture())
  end

  defp approved_beacon_fixture(user, attrs \\ %{}) do
    beacon = beacon_fixture(user, attrs)
    {:ok, beacon} = Beacons.approve_beacon(beacon)
    beacon
  end

  describe "Index (public)" do
    test "lists approved beacons without logging in", %{conn: conn} do
      beacon = approved_beacon_fixture(user_fixture())
      {:ok, _view, html} = live(conn, ~p"/beacons")
      assert html =~ "Beacons"
      assert html =~ beacon.callsign
    end

    test "hides unapproved beacons from the public list", %{conn: conn} do
      _pending = beacon_fixture(user_fixture(), callsign: "W5PEN")
      {:ok, _view, html} = live(conn, ~p"/beacons")
      refute html =~ "W5PEN"
    end

    test "anonymous sees the Submit Beacon button", %{conn: conn} do
      {:ok, _view, html} = live(conn, ~p"/beacons")
      assert html =~ "Submit Beacon"
    end

    test "authenticated user sees the Submit Beacon button", %{conn: conn} do
      user = user_fixture()
      conn = log_in_user(conn, user)
      {:ok, _view, html} = live(conn, ~p"/beacons")
      assert html =~ "Submit Beacon"
    end

    test "admin sees pending beacons section", %{conn: conn} do
      pending = beacon_fixture(user_fixture(), callsign: "W5PEN")
      conn = log_in_user(conn, admin_user_fixture())
      {:ok, _view, html} = live(conn, ~p"/beacons")
      assert html =~ "Pending approval"
      assert html =~ pending.callsign
    end

    test "non-admin does not see pending beacons section", %{conn: conn} do
      _pending = beacon_fixture(user_fixture(), callsign: "W5PEN")
      conn = log_in_user(conn, user_fixture())
      {:ok, _view, html} = live(conn, ~p"/beacons")
      refute html =~ "Pending approval"
      refute html =~ "W5PEN"
    end

    test "admin can approve a pending beacon", %{conn: conn} do
      pending = beacon_fixture(user_fixture(), callsign: "W5PEN")
      conn = log_in_user(conn, admin_user_fixture())
      {:ok, view, _html} = live(conn, ~p"/beacons")

      html =
        view
        |> element("a[phx-click][data-confirm='Approve this beacon?']")
        |> render_click()

      assert html =~ "Approved #{pending.callsign}"
      assert Beacons.get_beacon!(pending.id).approved == true
    end
  end

  describe "Show (public)" do
    test "renders beacon details without logging in", %{conn: conn} do
      beacon = approved_beacon_fixture(user_fixture())
      {:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")
      assert html =~ beacon.callsign
      assert html =~ "10,368.1"
    end

    test "shows pending badge on unapproved beacons", %{conn: conn} do
      beacon = beacon_fixture(user_fixture())
      {:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")
      assert html =~ "Pending approval"
    end

    test "plot-path-to-beacon link pre-populates tx_power + gain to match beacon EIRP",
         %{conn: conn} do
      # power_mw: 10_000 → EIRP 40 dBm → default-gain 30 dBi → tx 10 dBm
      beacon = approved_beacon_fixture(user_fixture(), power_mw: 10_000.0)
      {:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")

      assert html =~ "tx_power_dbm=10.0"
      assert html =~ "src_gain_dbi=30.0"
    end

    test "low-EIRP beacon clamps tx_power to 0 dBm floor and lowers gain", %{conn: conn} do
      # power_mw: 100 → EIRP 20 dBm → tx 0 dBm (clamped), gain 20 dBi
      beacon = approved_beacon_fixture(user_fixture(), power_mw: 100.0, callsign: "W5LOW")
      {:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")

      assert html =~ "tx_power_dbm=0.0"
      assert html =~ "src_gain_dbi=20.0"
    end
  end

  describe "Form" do
    test "authenticated non-admin can access /beacons/new", %{conn: conn} do
      user = user_fixture()
      conn = log_in_user(conn, user)
      {:ok, _view, html} = live(conn, ~p"/beacons/new")
      assert html =~ "New beacon"
    end

    test "anonymous user can access /beacons/new", %{conn: conn} do
      {:ok, _view, html} = live(conn, ~p"/beacons/new")
      assert html =~ "New beacon"
    end

    test "anonymous submission creates an unapproved beacon with no user_id", %{conn: conn} do
      {:ok, form_live, _html} = live(conn, ~p"/beacons/new")

      attrs = valid_beacon_attrs(callsign: "W5ANON")

      assert {:ok, _view, html} =
               form_live
               |> form("#beacon-form", beacon: attrs)
               |> render_submit()
               |> follow_redirect(conn, ~p"/beacons")

      assert html =~ "awaiting admin approval"

      beacon =
        Enum.find(Beacons.list_pending_beacons(), fn b -> b.callsign == "W5ANON" end)

      assert beacon
      assert beacon.user_id == nil
      assert beacon.approved == false
    end

    test "non-admin submission creates an unapproved beacon", %{conn: conn} do
      user = user_fixture()
      conn = log_in_user(conn, user)
      {:ok, form_live, _html} = live(conn, ~p"/beacons/new")

      attrs = valid_beacon_attrs()

      assert {:ok, _view, _html} =
               form_live
               |> form("#beacon-form", beacon: attrs)
               |> render_submit()
               |> follow_redirect(conn, ~p"/beacons")

      beacon = List.first(Beacons.list_pending_beacons())
      assert beacon.callsign == attrs.callsign
      assert beacon.approved == false
    end

    test "admin edits a beacon", %{conn: conn} do
      admin = admin_user_fixture()
      beacon = beacon_fixture(admin)
      conn = log_in_user(conn, admin)

      {:ok, form_live, _} = live(conn, ~p"/beacons/#{beacon}/edit")

      assert {:ok, _view, html} =
               form_live
               |> form("#beacon-form", beacon: %{power_mw: 25_000.0})
               |> render_submit()
               |> follow_redirect(conn, ~p"/beacons")

      assert html =~ "Beacon updated"
    end

    test "non-admin cannot access /beacons/:id/edit", %{conn: conn} do
      beacon = beacon_fixture(user_fixture())
      conn = log_in_user(conn, user_fixture())
      assert {:error, {:redirect, %{to: "/"}}} = live(conn, ~p"/beacons/#{beacon}/edit")
    end
  end

  describe "Show — lazy coverage" do
    test "does not compute the estimate or embed cells on mount", %{conn: conn} do
      beacon = approved_beacon_fixture(user_fixture(), frequency_mhz: 10_368.1)
      {:ok, view, html} = live(conn, ~p"/beacons/#{beacon}")

      # Map div exists but data-cells is absent or empty until the user
      # flips the toggle — we don't want to pay the RangeEstimate cost
      # on every page load.
      refute html =~ "data-cells"
      assert has_element?(view, "input[phx-click='toggle_coverage']")
    end

    test "flipping the toggle computes the estimate and pushes coverage data", %{conn: conn} do
      beacon = approved_beacon_fixture(user_fixture(), frequency_mhz: 10_368.1)
      {:ok, view, html} = live(conn, ~p"/beacons/#{beacon}")

      # Before the click the coverage info bar is absent.
      refute html =~ "cells rendered"

      render_click(element(view, "input[phx-click='toggle_coverage']"))
      html = render(view)

      # After toggling on, the coverage info bar (which only renders
      # when @estimate is populated and @show_coverage is true) shows.
      assert html =~ "cells rendered"
      assert html =~ "Atm loss"
    end

    test "toggle_coverage is a no-op for beacons below 5.76 GHz", %{conn: conn} do
      beacon = approved_beacon_fixture(user_fixture(), frequency_mhz: 1296.0)
      {:ok, view, html} = live(conn, ~p"/beacons/#{beacon}")

      assert html =~ "5.76 GHz and up only"
      refute html =~ "data-cells"
      refute html =~ "cells rendered"

      # Even if a client-side hack forces the click, the server refuses.
      render_click(view, "toggle_coverage")
      refute render(view) =~ "cells rendered"
    end
  end

  describe "Show — extra rendering + event coverage" do
    test "notes section renders when beacon has notes", %{conn: conn} do
      beacon =
        approved_beacon_fixture(user_fixture(), notes: "Co-located with WX radar, 12h/day.")

      {:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")

      assert html =~ "Notes"
      assert html =~ "Co-located with WX radar, 12h/day."
    end

    test "on_the_air=false renders the 'No' ghost badge instead of success", %{conn: conn} do
      beacon = approved_beacon_fixture(user_fixture(), on_the_air: false)
      {:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")

      assert html =~ "badge-ghost"
      # HEEx interpolation may emit whitespace — assert on the
      # class-to-text sequence rather than `>No<` tightly.
      assert Regex.match?(~r/badge-ghost[^>]*>\s*No\s*</s, html)
    end

    test "bearing is rendered as 'Omni' when stored as omni", %{conn: conn} do
      beacon = approved_beacon_fixture(user_fixture(), bearing: "omni")
      {:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")

      assert html =~ "Omni"
    end

    test "admin sees Approve button on unapproved beacon and the click approves it",
         %{conn: conn} do
      admin = admin_user_fixture()
      pending = beacon_fixture(admin)
      conn = log_in_user(conn, admin)

      {:ok, view, html} = live(conn, ~p"/beacons/#{pending}")
      assert html =~ "Approve"
      assert html =~ "Pending approval"

      render_click(element(view, "button", "Approve"))

      refute render(view) =~ "Pending approval"
      assert Beacons.get_beacon!(pending.id).approved == true
    end

    test "non-admin does not see the Approve button even on unapproved beacons",
         %{conn: conn} do
      user = user_fixture()
      pending = beacon_fixture(user)
      conn = log_in_user(conn, user)

      {:ok, _view, html} = live(conn, ~p"/beacons/#{pending}")
      assert html =~ "Pending approval"
      refute html =~ ">Approve<"
    end

    test "handle_info({:updated, beacon}) refreshes the displayed callsign", %{conn: conn} do
      beacon = approved_beacon_fixture(user_fixture(), callsign: "W5OLD")
      {:ok, view, html} = live(conn, ~p"/beacons/#{beacon}")
      assert html =~ "W5OLD"

      # Preload :user onto the updated struct — the production broadcast
      # path emits NotLoaded user and the template derefs beacon.user.callsign.
      updated =
        beacon
        |> Microwaveprop.Repo.preload(:user)
        |> Map.put(:callsign, "W5NEW")

      send(view.pid, {:updated, updated})

      assert render(view) =~ "W5NEW"
    end

    test "handle_info({:deleted, beacon}) redirects back to the index with a flash", %{conn: conn} do
      beacon = approved_beacon_fixture(user_fixture())
      {:ok, view, _html} = live(conn, ~p"/beacons/#{beacon}")

      send(view.pid, {:deleted, beacon})

      assert_redirect(view, ~p"/beacons")
    end

    test "handle_info for a different beacon id is a no-op", %{conn: conn} do
      beacon = approved_beacon_fixture(user_fixture(), callsign: "W5STAY")
      other = approved_beacon_fixture(user_fixture(), callsign: "W5OTHER")

      {:ok, view, _html} = live(conn, ~p"/beacons/#{beacon}")

      # An unrelated beacon update should NOT change the visible callsign.
      {:ok, other_updated} = Beacons.update_beacon(other, %{callsign: "W5DIFF"})
      send(view.pid, {:updated, other_updated})

      html = render(view)
      assert html =~ "W5STAY"
      refute html =~ "W5DIFF"
    end
  end

  describe "Show — edge cases on display fields" do
    test "numeric bearing renders with a degree symbol", %{conn: conn} do
      # bearing_label/1 is a 3-clause dispatch; numeric values hit the
      # fallback and produce "NNN°".
      beacon = approved_beacon_fixture(user_fixture(), bearing: "185")
      {:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")

      assert html =~ "185°"
    end

    test "'Plot path to beacon' link omits tx_power/gain for zero-power beacons",
         %{conn: conn} do
      # baseline_tx_for_eirp/1 has a `power_mw <= 0 -> {nil, nil}`
      # clause that maybe_put then drops from the URL query string.
      # Assert neither key appears when power_mw is 0.0.
      beacon =
        approved_beacon_fixture(
          user_fixture(),
          callsign: "W5ZER",
          power_mw: 0.0
        )

      {:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")

      assert html =~ "destination="
      refute html =~ "tx_power_dbm="
      refute html =~ "src_gain_dbi="
    end

    test "non-admin unauthenticated user clicking 'approve' is rejected with a flash",
         %{conn: conn} do
      # The approve handler is rendered for admins only but the server
      # still defends the admin check. Simulate a crafted client pushing
      # the event manually with no scope; the flash branch should fire.
      pending = beacon_fixture(user_fixture(), callsign: "W5DEF")

      {:ok, view, _html} = live(conn, ~p"/beacons/#{pending}")

      render_click(view, "approve")

      # Still pending — no admin promotion.
      refute Beacons.get_beacon!(pending.id).approved
      assert render(view) =~ "Admins only."
    end
  end

  describe "Show — property: URL beacon id round-trip" do
    property "mounting /beacons/:id for a created beacon always renders its callsign" do
      # Wrap each run in a sandbox checkout so Repo inserts are isolated.
      check all(suffix <- integer(1..1_000_000), max_runs: 5) do
        callsign = "W5P#{Integer.to_string(suffix, 36)}" |> String.upcase() |> String.slice(0, 6)
        beacon = approved_beacon_fixture(user_fixture(), callsign: callsign)

        conn = Phoenix.ConnTest.build_conn()
        {:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")

        assert html =~ callsign
      end
    end
  end
end
