defmodule Microwaveprop.Workers.ContactImportWorkerTest do
  use Microwaveprop.DataCase, async: false

  alias Microwaveprop.Radio.Contact
  alias Microwaveprop.Radio.CsvImport
  alias Microwaveprop.Radio.ImportRun
  alias Microwaveprop.Terrain.ElevationClient
  alias Microwaveprop.Weather.HrrrClient
  alias Microwaveprop.Weather.IemClient
  alias Microwaveprop.Workers.ContactImportWorker

  @valid_header "station1,station2,grid1,grid2,band,mode,qso_timestamp"
  @submitter "test@example.com"

  setup do
    Req.Test.stub(IemClient, fn conn ->
      case conn.request_path do
        "/cgi-bin/request/asos.py" -> Req.Test.text(conn, "#DEBUG,\nstation,valid,tmpf\n")
        _ -> Req.Test.json(conn, %{"profiles" => []})
      end
    end)

    Req.Test.stub(HrrrClient, fn conn ->
      Plug.Conn.send_resp(conn, 404, "not found")
    end)

    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 build_csv(rows) do
    Enum.join([@valid_header | rows], "\n")
  end

  defp insert_preview(csv) do
    {:ok, preview} = CsvImport.preview(csv, @submitter)
    {:ok, run_id} = CsvImport.enqueue(%{valid: preview.valid, refinements: preview.refinements}, @submitter)
    # enqueue/2 also inserts jobs — but with testing: :inline those jobs already
    # ran. We want a fresh run for the unit test, so delete the existing one
    # and re-create with the same shape.
    :ok = reset_run(run_id, preview)
    run_id
  end

  # Reset counters on the just-created run so we can drive the worker manually.
  defp reset_run(run_id, _preview) do
    {1, _} =
      Repo.update_all(from(r in ImportRun, where: r.id == ^run_id),
        set: [
          processed_rows: 0,
          imported_count: 0,
          refined_count: 0,
          error_count: 0,
          errors: %{},
          status: "pending",
          started_at: nil,
          completed_at: nil
        ]
      )

    Repo.delete_all(Contact)
    :ok
  end

  describe "perform/1 happy path" do
    test "processes a chunk, inserts contacts, and bumps counters" do
      csv =
        build_csv([
          "W5XD,K5TR,EM12,EM00,10000,CW,2026-03-28T18:00:00Z",
          "N5AC,W5LUA,EM13,EM20,24000,SSB,2026-03-28T19:00:00Z"
        ])

      run_id = insert_preview(csv)

      assert :ok =
               ContactImportWorker.perform(%Oban.Job{
                 args: %{"import_run_id" => run_id, "offset" => 0, "limit" => 2}
               })

      run = Repo.get!(ImportRun, run_id)
      assert run.processed_rows == 2
      assert run.imported_count == 2
      assert run.error_count == 0
      assert run.status == "complete"
      assert run.completed_at
      assert run.started_at

      assert Repo.aggregate(Contact, :count) == 2
    end

    test "flips status to running on first chunk, complete on last" do
      csv =
        build_csv([
          "W5XD,K5TR,EM12,EM00,10000,CW,2026-03-28T18:00:00Z",
          "N5AC,W5LUA,EM13,EM20,24000,SSB,2026-03-28T19:00:00Z",
          "W5XD,W5LUA,EM12,EM20,1296,CW,2026-03-28T20:00:00Z"
        ])

      run_id = insert_preview(csv)

      # First chunk of 1: marks running.
      :ok =
        ContactImportWorker.perform(%Oban.Job{
          args: %{"import_run_id" => run_id, "offset" => 0, "limit" => 1}
        })

      run = Repo.get!(ImportRun, run_id)
      assert run.status == "running"
      assert run.processed_rows == 1
      assert run.started_at
      refute run.completed_at

      # Final chunk: marks complete.
      :ok =
        ContactImportWorker.perform(%Oban.Job{
          args: %{"import_run_id" => run_id, "offset" => 1, "limit" => 2}
        })

      run = Repo.get!(ImportRun, run_id)
      assert run.status == "complete"
      assert run.processed_rows == 3
      assert run.completed_at
    end
  end

  describe "perform/1 errors" do
    test "a failing row counts into error_count without blocking siblings" do
      # First row has an existing contact we will try to re-insert via a
      # forced-bad row path; simpler: create a unique DB constraint failure
      # by inserting a duplicate row manually. We force a changeset error
      # by stuffing an invalid grid into a row — preview() rejects it, so
      # skip preview and hand the worker a hand-rolled ImportRun.
      rows = [
        %{
          "kind" => "insert",
          "row_num" => 2,
          "attrs" => %{
            "station1" => "W5XD",
            "station2" => "K5TR",
            "grid1" => "ZZZZ",
            "grid2" => "EM00",
            "band" => "10000",
            "mode" => "CW",
            "qso_timestamp" => "2026-03-28T18:00:00Z",
            "submitter_email" => @submitter
          },
          "timestamp" => "2026-03-28T18:00:00Z"
        },
        %{
          "kind" => "insert",
          "row_num" => 3,
          "attrs" => %{
            "station1" => "N5AC",
            "station2" => "W5LUA",
            "grid1" => "EM13",
            "grid2" => "EM20",
            "band" => "24000",
            "mode" => "SSB",
            "qso_timestamp" => "2026-03-28T19:00:00Z",
            "submitter_email" => @submitter
          },
          "timestamp" => "2026-03-28T19:00:00Z"
        }
      ]

      {:ok, run} =
        %ImportRun{}
        |> ImportRun.changeset(%{
          submitter_email: @submitter,
          rows: %{"rows" => rows, "refinement_ids" => []},
          total_rows: 2
        })
        |> Repo.insert()

      :ok =
        ContactImportWorker.perform(%Oban.Job{
          args: %{"import_run_id" => run.id, "offset" => 0, "limit" => 2}
        })

      reloaded = Repo.get!(ImportRun, run.id)
      assert reloaded.processed_rows == 2
      assert reloaded.imported_count == 1
      assert reloaded.error_count == 1
      assert reloaded.status == "complete"
      assert Map.has_key?(reloaded.errors, "2")
      assert [msg | _] = reloaded.errors["2"]
      assert is_binary(msg)
    end
  end

  describe "broadcast" do
    test "publishes progress to csv_import:<id> after each chunk" do
      csv = build_csv(["W5XD,K5TR,EM12,EM00,10000,CW,2026-03-28T18:00:00Z"])
      run_id = insert_preview(csv)

      :ok = Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "csv_import:#{run_id}")

      :ok =
        ContactImportWorker.perform(%Oban.Job{
          args: %{"import_run_id" => run_id, "offset" => 0, "limit" => 1}
        })

      assert_receive {:import_progress, ^run_id, %ImportRun{status: "complete", processed_rows: 1}}
    end
  end

  describe "missing run" do
    test "returns :ok for a deleted import run" do
      run_id = Ecto.UUID.generate()

      assert :ok =
               ContactImportWorker.perform(%Oban.Job{
                 args: %{"import_run_id" => run_id, "offset" => 0, "limit" => 1}
               })
    end
  end

  # Reproduces a bug where the preview screen correctly flags an upload row
  # as a duplicate of an existing DB contact (via direction-agnostic matching)
  # but the async worker ends up inserting it anyway. The full pipeline —
  # preview -> enqueue -> worker -> DB — must honor the preview's exclusion
  # so the Imported count on /imports/:id never exceeds what the preview
  # claimed as "Valid".
  describe "preview-to-worker dedup parity" do
    test "worker does not insert rows the preview classified as duplicates" do
      {:ok, _existing} =
        %Contact{}
        |> Contact.submission_changeset(%{
          "station1" => "W5XD",
          "station2" => "K5TR",
          "grid1" => "EM12",
          "grid2" => "EM00",
          "band" => "10000",
          "mode" => "CW",
          "qso_timestamp" => "2026-03-28T18:00:00Z",
          "submitter_email" => @submitter
        })
        |> Repo.insert()

      # Upload row is a direction-swapped version of the existing contact
      # at the same time/band — should be flagged as a duplicate.
      csv = build_csv(["K5TR,W5XD,EM00,EM12,10000,CW,2026-03-28T18:30:00Z"])

      {:ok, preview} = CsvImport.preview(csv, @submitter)
      assert preview.valid == []
      assert length(preview.duplicates) == 1

      {:ok, run_id} = CsvImport.enqueue(preview, @submitter)

      run = Repo.get!(ImportRun, run_id)
      assert run.total_rows == 0
      assert run.rows["rows"] == []

      assert Repo.aggregate(Contact, :count) == 1
    end

    test "worker does not insert in-upload direction-swapped duplicates" do
      # Same contact logged from both ends — the CSV has row A->B and
      # B->A at the same time/band. Only one should land in the DB.
      csv =
        build_csv([
          "W5XD,K5TR,EM12,EM00,10000,CW,2026-03-28T18:00:00Z",
          "K5TR,W5XD,EM00,EM12,10000,CW,2026-03-28T18:00:00Z"
        ])

      {:ok, preview} = CsvImport.preview(csv, @submitter)
      assert length(preview.valid) == 1
      assert length(preview.duplicates) == 1

      {:ok, run_id} = CsvImport.enqueue(preview, @submitter)

      run = Repo.get!(ImportRun, run_id)
      assert run.total_rows == 1
      assert length(run.rows["rows"]) == 1

      assert Repo.aggregate(Contact, :count) == 1
    end

    # Covers the race window: preview ran when no matching contact existed,
    # but by the time the worker picks up the chunk another insert (from a
    # concurrent upload, a retry, etc.) has already landed. `Radio.create_contact`
    # now returns `{:error, :duplicate, existing}`. The worker must not crash,
    # must not double-insert, and must keep the counters in a sensible state.
    test "worker survives when a row's duplicate appears in DB after preview" do
      {:ok, _existing} =
        %Contact{}
        |> Contact.submission_changeset(%{
          "station1" => "W5XD",
          "station2" => "K5TR",
          "grid1" => "EM12",
          "grid2" => "EM00",
          "band" => "10000",
          "mode" => "CW",
          "qso_timestamp" => "2026-03-28T18:00:00Z",
          "submitter_email" => @submitter
        })
        |> Repo.insert()

      # Hand-rolled ImportRun: simulates a preview where the row slipped
      # through as "valid" (dup appeared after preview ran).
      rows = [
        %{
          "kind" => "insert",
          "row_num" => 2,
          "attrs" => %{
            "station1" => "W5XD",
            "station2" => "K5TR",
            "grid1" => "EM12",
            "grid2" => "EM00",
            "band" => "10000",
            "mode" => "CW",
            "qso_timestamp" => "2026-03-28T18:00:00Z",
            "submitter_email" => @submitter
          },
          "timestamp" => "2026-03-28T18:00:00Z"
        }
      ]

      {:ok, run} =
        %ImportRun{}
        |> ImportRun.changeset(%{
          submitter_email: @submitter,
          rows: %{"rows" => rows, "refinement_ids" => []},
          total_rows: 1
        })
        |> Repo.insert()

      assert :ok =
               ContactImportWorker.perform(%Oban.Job{
                 args: %{"import_run_id" => run.id, "offset" => 0, "limit" => 1}
               })

      reloaded = Repo.get!(ImportRun, run.id)
      assert reloaded.processed_rows == 1
      assert reloaded.imported_count == 0
      assert reloaded.status == "complete"

      # The existing contact must remain; no new row inserted.
      assert Repo.aggregate(Contact, :count) == 1
    end
  end
end
