defmodule MicrowavepropWeb.RoverPlanningLiveTest do
  use MicrowavepropWeb.ConnCase, async: true

  import Phoenix.LiveViewTest

  alias Microwaveprop.AccountsFixtures
  alias Microwaveprop.Radio.Maidenhead
  alias Microwaveprop.Repo
  alias Microwaveprop.Rover
  alias Microwaveprop.Rover.Location
  alias Microwaveprop.RoverPlanning
  alias Microwaveprop.RoverPlanning.Path
  alias Microwaveprop.Terrain.ElevationClient

  setup do
    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)

    :ok
  end

  defp create_mission(user, name, opts \\ []) do
    locations = Keyword.get(opts, :locations, [%{lat: 32.0, lon: -97.0, status: :good}])
    stations = Keyword.get(opts, :stations, %{"0" => %{"input" => "EM12kp", "position" => 0}})

    Enum.each(locations, fn attrs -> {:ok, _} = Rover.create_location(user, attrs) end)

    {:ok, mission} =
      RoverPlanning.create_mission(user, %{
        "name" => name,
        "band_mhz" => 10_000,
        "only_known_good" => true,
        "rover_height_ft" => 8.0,
        "station_height_ft" => 30.0,
        "stations" => stations
      })

    mission
  end

  describe "index" do
    test "anonymous visitor sees the table and a sign-in prompt", %{conn: conn} do
      {:ok, _lv, html} = live(conn, ~p"/rover-planning")
      assert html =~ "Rover Planning"
      assert html =~ "Sign in"
    end

    test "logged-in user sees the New mission button", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      conn = log_in_user(conn, user)
      {:ok, _lv, html} = live(conn, ~p"/rover-planning")
      assert html =~ "New mission"
    end

    test "table shows existing missions and links to the show page", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "Visible mission")

      {:ok, _lv, html} = live(conn, ~p"/rover-planning")
      assert html =~ "Visible mission"
      assert html =~ ~s(href="/rover-planning/#{mission.id}")
    end
  end

  describe "show" do
    test "renders mission details + path table", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "Detail mission")

      {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")
      assert html =~ "Detail mission"
      assert html =~ "Stationary stations"
      assert html =~ "Path profiles"
    end

    test "group heading uses max-precision Maidenhead grid (10 chars)", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "Precision mission")

      {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")

      # Maidenhead.from_latlon(32.0, -97.0, 10) → "EM12VX00AA" (or similar
      # 10-char grid). The group heading must NOT truncate at 6 chars.
      grid_10 = Maidenhead.from_latlon(32.0, -97.0, 10)
      assert String.length(grid_10) == 10
      assert html =~ grid_10
    end

    test "station label uses derived grid when only lat/lon are stored", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      {:ok, _} = Rover.create_location(user, %{lat: 32.0, lon: -97.0, status: :good})

      # User enters raw coords (no callsign or grid lookup) → station ends
      # up with lat/lon set but callsign/grid blank.
      {:ok, mission} =
        RoverPlanning.create_mission(user, %{
          "name" => "Coord-only station mission",
          "band_mhz" => 10_000,
          "only_known_good" => true,
          "rover_height_ft" => 8.0,
          "station_height_ft" => 30.0,
          "stations" => %{"0" => %{"input" => "33.189,-96.452", "position" => 0}}
        })

      {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")

      derived = Maidenhead.from_latlon(33.189, -96.452, 6)
      assert html =~ derived
      # The path-profiles "Station" cell must render the derived grid,
      # not the bare lat/lon. Match the exact <td>…</td> shape so we
      # don't trip on the Stationary-stations list at the top of the
      # page (which legitimately shows lat/lon as a secondary line).
      refute html =~ ~s(<td class="font-mono text-xs">33.189, -96.452</td>)
    end

    test "station cell renders callsign + grid as separate lines", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "Callsign-grid mission")

      [path | _] = Path |> Repo.all() |> Repo.preload(:station)
      {:ok, _} = path.station |> Ecto.Changeset.change(%{callsign: "AA5C", grid: "EM13se"}) |> Repo.update()

      {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")

      # Callsign and grid both appear, on separate lines (callsign primary,
      # grid as a muted secondary). The legacy one-liner "AA5C · EM13se"
      # no longer appears anywhere.
      assert html =~ "AA5C"
      assert html =~ "EM13se"
      refute html =~ "AA5C · EM13se"
    end

    test "verdict column renders the badge for completed paths", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "Verdict mission")

      [path | _] = Repo.all(Path)

      {:ok, _} =
        path
        |> Ecto.Changeset.change(%{
          status: :complete,
          result: %{
            "distance_km" => 12.0,
            "verdict" => "CLEAR",
            "min_clearance_m" => 50.0,
            "diffraction_db" => 0.0
          }
        })
        |> Repo.update()

      {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")
      assert html =~ "Clear"
    end

    test "verdict column maps BLOCKED + FRESNEL_PARTIAL to badges", %{conn: conn} do
      user = AccountsFixtures.user_fixture()

      mission =
        create_mission(user, "Blocked mission",
          locations: [
            %{lat: 32.0, lon: -97.0, status: :good},
            %{lat: 33.0, lon: -96.0, status: :good}
          ]
        )

      [p1, p2] = Repo.all(Path)

      {:ok, _} =
        p1
        |> Ecto.Changeset.change(%{
          status: :complete,
          result: %{"distance_km" => 8.0, "verdict" => "BLOCKED"}
        })
        |> Repo.update()

      {:ok, _} =
        p2
        |> Ecto.Changeset.change(%{
          status: :complete,
          result: %{"distance_km" => 9.0, "verdict" => "FRESNEL_PARTIAL"}
        })
        |> Repo.update()

      {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")
      assert html =~ "Blocked"
      assert html =~ "Fresnel"
    end

    test "propagation column renders score badge for completed paths", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "Score column mission")

      [path | _] = Repo.all(Path)

      {:ok, _} =
        path
        |> Ecto.Changeset.change(%{
          status: :complete,
          result: %{
            "distance_km" => 12.0,
            "verdict" => "CLEAR",
            "propagation_score" => 78
          }
        })
        |> Repo.update()

      {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")
      assert html =~ "Propagation"
      assert html =~ "78"
    end

    test "loss column renders the cached total baseline path loss", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "Loss column mission")

      [path | _] = Repo.all(Path)

      {:ok, _} =
        path
        |> Ecto.Changeset.change(%{
          status: :complete,
          result: %{
            "distance_km" => 12.0,
            "verdict" => "CLEAR",
            "free_space_loss_db" => 134.7,
            "oxygen_loss_db" => 0.08,
            "humidity_loss_db_baseline" => 0.18,
            "total_baseline_loss_db" => 135.0
          }
        })
        |> Repo.update()

      {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")
      assert html =~ "Loss"
      # The TOTAL is what the user actually cares about at-a-glance —
      # FSPL alone undersells humid-band paths. Show with one decimal +
      # "dB" suffix.
      assert html =~ "135.0 dB"
    end

    test "propagation column renders dash when score is missing", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "No-score mission")

      [path | _] = Repo.all(Path)

      {:ok, _} =
        path
        |> Ecto.Changeset.change(%{
          status: :complete,
          result: %{"distance_km" => 12.0, "verdict" => "CLEAR", "propagation_score" => nil}
        })
        |> Repo.update()

      {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")
      assert html =~ "Propagation"
    end

    test "stationary stations list shows uppercase callsign + grid, not bare lat/lon", %{conn: conn} do
      import Ecto.Query, only: [from: 2]

      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "Stationary label mission")

      # Backdate the station to mimic legacy data: callsign persisted in
      # lowercase, no stored grid. The display should still render the
      # callsign uppercase and synthesize a grid from lat/lon.
      mission_id = mission.id

      [s1] =
        Repo.all(from s in Microwaveprop.RoverPlanning.Station, where: s.mission_id == ^mission_id)

      {:ok, _} =
        s1
        |> Ecto.Changeset.change(%{callsign: "aa5c", grid: nil, lat: 33.1889, lon: -96.4517})
        |> Repo.update()

      {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")

      assert html =~ "AA5C"
      # Bare lat/lon must not appear in the stationary-stations list.
      refute html =~ "33.1889, -96.4517"
      # A grid (derived from lat/lon when none is stored) IS shown.
      assert html =~ Maidenhead.from_latlon(33.1889, -96.4517, 6)
    end

    test "rover-location group heading links to /rover-locations/:id", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "Linked heading mission")

      [path | _] = Repo.all(Path)
      loc_id = path.rover_location_id

      {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")
      # The heading should be a clickable link to the rover-location's
      # detail page so the user can quickly inspect / edit the spot.
      assert html =~ ~s(href="/rover-locations/#{loc_id}")
    end

    test "path table omits min clearance + diffraction columns", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "Trim columns mission")

      {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")
      # Header columns relevant to a planning summary: Station / Status /
      # Distance / Verdict. Min clearance + diffraction are detail-level
      # values better viewed on the Path Calculator itself.
      refute html =~ "Min clearance"
      refute html =~ "Diffraction"
    end

    test "path rows link to the cached stored-path detail page", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "Click-through mission")

      {:ok, lv, _html} = live(conn, ~p"/rover-planning/#{mission.id}")

      [path | _] = Repo.all(Path)
      rendered = render(lv)

      # Each row navigates to /path?rover_path_id=UUID — PathLive
      # detects the param, deserializes the worker-cached compute
      # output, and skips the live recompute entirely.
      assert rendered =~ "phx-click"
      assert rendered =~ "/path?rover_path_id=#{path.id}"
    end

    test "groups paths by rover location", %{conn: conn} do
      user = AccountsFixtures.user_fixture()

      mission =
        create_mission(user, "Grouped mission",
          locations: [
            %{lat: 32.0, lon: -97.0, status: :good, notes: "Spot Alpha"},
            %{lat: 33.0, lon: -98.0, status: :good, notes: "Spot Bravo"}
          ],
          stations: %{
            "0" => %{"input" => "EM12kp", "position" => 0},
            "1" => %{"input" => "EM13qc", "position" => 1}
          }
        )

      {:ok, lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")

      # One group container per rover location; both lat/lon labels appear
      # as group headings (not inline in cells like the old flat table).
      assert has_element?(lv, "[data-rover-group]")
      assert html =~ "32.0"
      assert html =~ "33.0"

      # 2 locations × 2 stations = 4 path rows split into 2 groups.
      group_count =
        lv
        |> render()
        |> then(&Regex.scan(~r/data-rover-group/, &1))
        |> length()

      assert group_count == 2
    end

    test "renders integer path-result values without crashing", %{conn: conn} do
      # JSONB round-trips bare zeros as integers (e.g. `0` not `0.0`), so the
      # show page MUST tolerate non-float numerics in `result`. Previously
      # `Float.round(value, n)` crashed with FunctionClauseError when value
      # came back as integer 0.
      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "Integer result mission")

      [path | _] = Repo.all(Path)

      {:ok, _} =
        path
        |> Ecto.Changeset.change(%{
          status: :complete,
          result: %{
            "distance_km" => 0,
            "min_clearance_m" => 0,
            "diffraction_db" => 0,
            "verdict" => "CLEAR"
          }
        })
        |> Repo.update()

      {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")
      assert html =~ "Integer result mission"
      assert html =~ "Clear"
    end

    test "owner can add a rover site from the show page", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "Add-site mission")
      conn = log_in_user(conn, user)

      {:ok, lv, _html} = live(conn, ~p"/rover-planning/#{mission.id}")

      html =
        lv
        |> form("form[phx-submit='add_rover_site']", %{"input" => "33.5, -97.5"})
        |> render_submit()

      # New site appears in the rover-sites list, and a flash confirms.
      assert html =~ "Rover site added"
      assert html =~ Maidenhead.from_latlon(33.5, -97.5, 10)
      assert Repo.aggregate(Location, :count) == 2
    end

    test "typing a new rover site surfaces an existing match by 6-char grid",
         %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "Match-existing mission")
      conn = log_in_user(conn, user)

      # Existing site at a known grid; the user-typed coords below
      # land in the same 6-char Maidenhead cell, so the form should
      # surface a link to the existing site rather than letting the
      # user duplicate it.
      {:ok, existing} =
        Rover.create_location(user, %{
          lat: 32.5,
          lon: -97.5,
          status: :good,
          notes: "Existing parking spot"
        })

      {:ok, lv, _html} = live(conn, ~p"/rover-planning/#{mission.id}")

      html =
        lv
        |> form("form[phx-submit='add_rover_site']", %{"input" => "32.5004, -97.4998"})
        |> render_change()

      # The 6-char grid for both points is identical, so a match link
      # to the existing site appears with its grid label.
      assert html =~ ~s(href="/rover-locations/#{existing.id})
      grid6 = Maidenhead.from_latlon(32.5, -97.5, 6)
      assert html =~ grid6
    end

    test "non-matching input shows no existing-site link", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "No-match mission")
      conn = log_in_user(conn, user)

      {:ok, _other} =
        Rover.create_location(user, %{lat: 32.5, lon: -97.5, status: :good})

      {:ok, lv, _html} = live(conn, ~p"/rover-planning/#{mission.id}")

      html =
        lv
        |> form("form[phx-submit='add_rover_site']", %{"input" => "45.0, -120.0"})
        |> render_change()

      refute html =~ "Already saved as"
    end

    test "owner can remove a rover site from the show page", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "Remove-site mission")
      conn = log_in_user(conn, user)

      [site] = Repo.all(Location)

      {:ok, lv, _html} = live(conn, ~p"/rover-planning/#{mission.id}")

      html =
        lv
        |> element("button[phx-click='delete_rover_site'][phx-value-id='#{site.id}']")
        |> render_click()

      assert html =~ "Rover site removed"
      assert Repo.aggregate(Location, :count) == 0
    end

    test "delete button lives inside the path-profile group heading, not above it",
         %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "Per-group delete mission")
      conn = log_in_user(conn, user)

      [site] = Repo.all(Location)

      {:ok, lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")

      # The standalone "Rover sites" list is gone — the heading now reads
      # "Add rover site" (still the form). Each path-profile group
      # heading carries its own per-location delete button. Asserting on
      # the structural ancestor (`[data-rover-group]`) ensures the button
      # is INSIDE the grouping, not in some legacy list above it.
      refute html =~ "Rover sites</h3>"
      assert html =~ "Add rover site</h3>"

      assert has_element?(
               lv,
               "[data-rover-group] button[phx-click='delete_rover_site'][phx-value-id='#{site.id}']"
             )
    end

    test "add_rover_site rejects whitespace-only input with a flash error",
         %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "Empty-input mission")
      conn = log_in_user(conn, user)

      {:ok, lv, _} = live(conn, ~p"/rover-planning/#{mission.id}")

      html =
        lv
        |> form("form[phx-submit='add_rover_site']", %{"input" => "   "})
        |> render_submit()

      # `   ` trims to "" → LocationResolver returns :empty, which the
      # handler maps to this flash. No new location was created.
      assert html =~ "Enter a callsign, grid, or lat,lon"
      assert Repo.aggregate(Location, :count) == 1
    end

    test "delete_rover_site shows error when the user does not own the site",
         %{conn: conn} do
      owner = AccountsFixtures.user_fixture()
      mission = create_mission(owner, "Foreign-site mission")

      [site] = Repo.all(Location)

      visitor = AccountsFixtures.user_fixture()
      conn = log_in_user(conn, visitor)

      {:ok, lv, _html} = live(conn, ~p"/rover-planning/#{mission.id}")

      # Visitor doesn't own the site, so the trash button is hidden;
      # but we exercise the server-side guard directly via render_hook
      # to confirm the not_found branch.
      html = render_hook(lv, "delete_rover_site", %{"id" => site.id})

      assert html =~ "You can only delete rover sites you created"
      assert Repo.aggregate(Location, :count) == 1
    end

    test "anonymous visitor sees a sign-in prompt instead of the add form", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      mission = create_mission(user, "Anon-view mission")

      {:ok, lv, _html} = live(conn, ~p"/rover-planning/#{mission.id}")

      refute has_element?(lv, "form[phx-submit='add_rover_site']")
      assert has_element?(lv, "a", "Sign in")
    end

    test "redirects when mission missing", %{conn: conn} do
      missing = Ecto.UUID.generate()

      assert {:error, {:live_redirect, %{to: "/rover-planning"}}} =
               live(conn, ~p"/rover-planning/#{missing}")
    end
  end

  describe "form" do
    test "anonymous user gets redirected to /rover-planning", %{conn: conn} do
      assert {:error, {:live_redirect, %{to: "/rover-planning"}}} =
               live(conn, ~p"/rover-planning/new")
    end

    test "edit-mode remove_station deletes ONLY the targeted station", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      _loc = AccountsFixtures.user_fixture()

      mission =
        create_mission(user, "Multi-station edit",
          stations: %{
            "0" => %{"input" => "EM12kp", "position" => 0},
            "1" => %{"input" => "EM13ks", "position" => 1},
            "2" => %{"input" => "EM14lr", "position" => 2}
          }
        )

      conn = log_in_user(conn, user)
      {:ok, lv, _html} = live(conn, ~p"/rover-planning/#{mission.id}/edit")

      # Click the trash icon on the second station (index 1) without
      # any prior validate firing — this was the bug: an empty
      # current_params + cast_assoc(on_replace: :delete) wiped every
      # existing station instead of just the targeted index.
      _html =
        lv
        |> element("button[phx-click='remove_station'][phx-value-index='1']")
        |> render_click()

      # Save and assert only stations 0 and 2 remain.
      {:error, {:live_redirect, _}} =
        lv
        |> form("#mission-form")
        |> render_submit()

      reloaded = Repo.preload(Repo.get!(Microwaveprop.RoverPlanning.Mission, mission.id), :stations)
      assert length(reloaded.stations) == 2
    end

    test "clicking 'Add station' does not show 'can't be blank' on the new row", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      conn = log_in_user(conn, user)
      {:ok, lv, _html} = live(conn, ~p"/rover-planning/new")

      html =
        lv
        |> element("button[phx-click='add_station']")
        |> render_click()

      # The new (untouched) station must not render a validation error until
      # the user actually interacts with its input.
      refute html =~ "can&#39;t be blank"
    end

    test "typing into the form does not flip the only_known_good checkbox", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      conn = log_in_user(conn, user)
      {:ok, lv, _html} = live(conn, ~p"/rover-planning/new")

      html =
        lv
        |> form("#mission-form",
          mission: %{
            name: "Some name",
            bands_mhz: ["10000"],
            rover_height_ft: "8.0",
            station_height_ft: "30.0",
            only_known_good: "true",
            stations: %{"0" => %{input: ""}}
          }
        )
        |> render_change()

      # The default-checked box must STILL be checked after a phx-change.
      # Hidden "false" + checkbox "true" with hidden first = checkbox wins.
      assert html =~
               ~s(name="mission[only_known_good]" value="true" checked)
    end

    test "remove_station drops the targeted station from the form", %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      conn = log_in_user(conn, user)
      {:ok, lv, _html} = live(conn, ~p"/rover-planning/new")

      # The new form seeds one station; click Add to get a second.
      _ = lv |> element("button[phx-click='add_station']") |> render_click()
      assert render(lv) =~ ~s(phx-value-index="1")

      html =
        lv
        |> element("button[phx-click='remove_station'][phx-value-index='1']")
        |> render_click()

      refute html =~ ~s(phx-value-index="1")
      assert html =~ ~s(phx-value-index="0")
    end

    test "add_station appends a new row on the very first click", %{conn: conn} do
      # Regression: before seeding initial params with one station, the
      # first add_station click was a no-op (cast_assoc replaced the
      # default Station struct with the new params row instead of
      # appending). Both indices must render after a single click.
      user = AccountsFixtures.user_fixture()
      conn = log_in_user(conn, user)
      {:ok, lv, _html} = live(conn, ~p"/rover-planning/new")

      html = lv |> element("button[phx-click='add_station']") |> render_click()

      assert html =~ ~s(phx-value-index="0")
      assert html =~ ~s(phx-value-index="1")
    end

    test "logged-in user can create a mission with a callsign-input station",
         %{conn: conn} do
      user = AccountsFixtures.user_fixture()
      {:ok, _} = Rover.create_location(user, %{lat: 32.0, lon: -97.0, status: :good})
      conn = log_in_user(conn, user)

      {:ok, lv, html} = live(conn, ~p"/rover-planning/new")
      assert html =~ "Only check against known good locations"
      assert html =~ ~s(checked)

      {:error, {:live_redirect, %{to: redirect_to}}} =
        lv
        |> form("#mission-form",
          mission: %{
            name: "From form",
            bands_mhz: ["10000"],
            only_known_good: "true",
            rover_height_ft: "8.0",
            station_height_ft: "30.0",
            stations: %{
              "0" => %{input: "EM12kp"}
            }
          }
        )
        |> render_submit()

      assert redirect_to =~ ~r{^/rover-planning/[0-9a-f-]+$}
    end
  end
end
