defmodule Microwaveprop.Qrz.ClientTest do
  # The Qrz.Client Agent is started once in the app supervision tree and
  # cached session state is process-global, so tests cannot run concurrently.
  use ExUnit.Case, async: false

  alias Microwaveprop.Qrz.Client

  @session_xml """
  <?xml version="1.0" encoding="utf-8"?>
  <QRZDatabase version="1.34">
    <Session>
      <Key>2331uf894c4bd29f3923f3bacf02c532d7bd9</Key>
      <Count>61</Count>
      <SubExp>Wed Jan 1 12:34:03 2025</SubExp>
      <GMTime>Sun Nov 16 04:48:02 2025</GMTime>
    </Session>
  </QRZDatabase>
  """

  @callsign_xml """
  <?xml version="1.0" encoding="utf-8"?>
  <QRZDatabase version="1.34">
    <Callsign>
      <call>W1AW</call>
      <fname>Hiram Percy</fname>
      <name>Maxim</name>
      <addr2>Newington</addr2>
      <state>CT</state>
      <country>United States</country>
      <grid>FN31pr</grid>
      <lat>41.714775</lat>
      <lon>-72.727260</lon>
    </Callsign>
    <Session>
      <Key>2331uf894c4bd29f3923f3bacf02c532d7bd9</Key>
      <Count>62</Count>
      <GMTime>Sun Nov 16 04:48:02 2025</GMTime>
    </Session>
  </QRZDatabase>
  """

  @session_expired_xml """
  <?xml version="1.0" encoding="utf-8"?>
  <QRZDatabase version="1.34">
    <Session>
      <Error>Session Timeout</Error>
      <GMTime>Sun Nov 16 04:48:02 2025</GMTime>
    </Session>
  </QRZDatabase>
  """

  @bad_credentials_xml """
  <?xml version="1.0" encoding="utf-8"?>
  <QRZDatabase version="1.34">
    <Session>
      <Error>Username/password incorrect</Error>
      <GMTime>Sun Nov 16 04:48:02 2025</GMTime>
    </Session>
  </QRZDatabase>
  """

  @not_found_xml """
  <?xml version="1.0" encoding="utf-8"?>
  <QRZDatabase version="1.34">
    <Session>
      <Key>2331uf894c4bd29f3923f3bacf02c532d7bd9</Key>
      <Error>Not found: NOSUCHCALL</Error>
      <GMTime>Sun Nov 16 04:48:02 2025</GMTime>
    </Session>
  </QRZDatabase>
  """

  setup do
    Client.reset_session()
    :ok
  end

  describe "login/0" do
    test "stores the session key returned by QRZ" do
      config = Application.get_env(:microwaveprop, Client, [])
      expected_username = Keyword.fetch!(config, :username)
      expected_password = Keyword.fetch!(config, :password)
      expected_agent = Keyword.get(config, :agent, "microwaveprop/1.0")

      Req.Test.stub(Client, fn conn ->
        params = Plug.Conn.fetch_query_params(conn).query_params
        assert params["username"] == expected_username
        assert params["password"] == expected_password
        assert params["agent"] == expected_agent
        Plug.Conn.send_resp(conn, 200, @session_xml)
      end)

      assert {:ok, "2331uf894c4bd29f3923f3bacf02c532d7bd9"} = Client.login()
    end

    test "returns {:error, message} when credentials are rejected" do
      Req.Test.stub(Client, fn conn ->
        Plug.Conn.send_resp(conn, 200, @bad_credentials_xml)
      end)

      assert {:error, "Username/password incorrect"} = Client.login()
    end

    test "returns {:error, _} on non-200 HTTP responses" do
      Req.Test.stub(Client, fn conn ->
        Plug.Conn.send_resp(conn, 500, "boom")
      end)

      assert {:error, "HTTP 500"} = Client.login()
    end

    test "returns {:error, _} on transport failures" do
      Req.Test.stub(Client, fn conn ->
        Req.Test.transport_error(conn, :econnrefused)
      end)

      assert {:error, "Request failed: " <> _} = Client.login()
    end
  end

  describe "lookup/1 with no cached session" do
    test "logs in transparently and returns a parsed callsign map" do
      Req.Test.expect(Client, 2, fn conn ->
        params = Plug.Conn.fetch_query_params(conn).query_params

        cond do
          Map.has_key?(params, "username") ->
            Plug.Conn.send_resp(conn, 200, @session_xml)

          Map.has_key?(params, "callsign") ->
            assert params["callsign"] == "W1AW"
            assert params["s"] == "2331uf894c4bd29f3923f3bacf02c532d7bd9"
            Plug.Conn.send_resp(conn, 200, @callsign_xml)
        end
      end)

      assert {:ok, record} = Client.lookup("W1AW")
      assert record["call"] == "W1AW"
      assert record["fname"] == "Hiram Percy"
      assert record["name"] == "Maxim"
      assert record["grid"] == "FN31pr"
      assert record["lat"] == "41.714775"
      assert record["lon"] == "-72.727260"
    end
  end

  describe "lookup/1 with a cached session" do
    test "skips login and uses the cached key" do
      # First call seeds the session cache.
      Req.Test.expect(Client, 2, fn conn ->
        params = Plug.Conn.fetch_query_params(conn).query_params

        cond do
          Map.has_key?(params, "username") ->
            Plug.Conn.send_resp(conn, 200, @session_xml)

          Map.has_key?(params, "callsign") ->
            Plug.Conn.send_resp(conn, 200, @callsign_xml)
        end
      end)

      assert {:ok, _} = Client.lookup("W1AW")

      # Now only the lookup request should hit the stub — no login.
      Req.Test.expect(Client, 1, fn conn ->
        params = Plug.Conn.fetch_query_params(conn).query_params
        refute Map.has_key?(params, "username")
        assert params["s"] == "2331uf894c4bd29f3923f3bacf02c532d7bd9"
        Plug.Conn.send_resp(conn, 200, @callsign_xml)
      end)

      assert {:ok, record} = Client.lookup("W1AW")
      assert record["call"] == "W1AW"
    end
  end

  describe "lookup/1 session expiry" do
    test "re-logs-in and retries when QRZ returns session_expired" do
      # Prime the cache with a known stale key.
      Req.Test.expect(Client, 1, fn conn ->
        Plug.Conn.send_resp(conn, 200, @session_xml)
      end)

      assert {:ok, _} = Client.login()

      # Sequence of responses for the retry flow:
      #   1. lookup with stale key → session_expired XML
      #   2. re-login → fresh session XML
      #   3. lookup with new key → success
      {:ok, agent} = Agent.start_link(fn -> 0 end)

      Req.Test.stub(Client, fn conn ->
        n = Agent.get_and_update(agent, fn n -> {n + 1, n + 1} end)
        params = Plug.Conn.fetch_query_params(conn).query_params

        case n do
          1 ->
            assert Map.has_key?(params, "callsign")
            Plug.Conn.send_resp(conn, 200, @session_expired_xml)

          2 ->
            assert Map.has_key?(params, "username")
            Plug.Conn.send_resp(conn, 200, @session_xml)

          3 ->
            assert Map.has_key?(params, "callsign")
            Plug.Conn.send_resp(conn, 200, @callsign_xml)
        end
      end)

      assert {:ok, record} = Client.lookup("W1AW")
      assert record["call"] == "W1AW"
      assert Agent.get(agent, & &1) == 3
    end

    test "returns an error when a retry login also expires" do
      # Prime with a cached key.
      Req.Test.expect(Client, 1, fn conn ->
        Plug.Conn.send_resp(conn, 200, @session_xml)
      end)

      assert {:ok, _} = Client.login()

      # Every response lacks a Session/Key, so the retried lookup still sees
      # :session_expired and the client bails out.
      Req.Test.stub(Client, fn conn ->
        Plug.Conn.send_resp(conn, 200, @session_expired_xml)
      end)

      assert {:error, _} = Client.lookup("W1AW")
    end
  end

  describe "lookup/1 error responses" do
    test "returns a not-found error when QRZ reports an unknown callsign" do
      Req.Test.expect(Client, 2, fn conn ->
        params = Plug.Conn.fetch_query_params(conn).query_params

        cond do
          Map.has_key?(params, "username") ->
            Plug.Conn.send_resp(conn, 200, @session_xml)

          Map.has_key?(params, "callsign") ->
            Plug.Conn.send_resp(conn, 200, @not_found_xml)
        end
      end)

      assert {:error, "Not found: NOSUCHCALL"} = Client.lookup("NOSUCHCALL")
    end

    test "bubbles up HTTP errors from the lookup request" do
      # Seed the cache so the lookup is the only request we need to stub.
      Req.Test.expect(Client, 1, fn conn ->
        Plug.Conn.send_resp(conn, 200, @session_xml)
      end)

      assert {:ok, _} = Client.login()

      Req.Test.stub(Client, fn conn ->
        Plug.Conn.send_resp(conn, 503, "service unavailable")
      end)

      assert {:error, "HTTP 503"} = Client.lookup("W1AW")
    end
  end

  describe "reset_session/0" do
    test "clears a cached session so the next lookup re-logs-in" do
      Req.Test.expect(Client, 1, fn conn ->
        Plug.Conn.send_resp(conn, 200, @session_xml)
      end)

      assert {:ok, _} = Client.login()
      assert :ok = Client.reset_session()

      # Because the cache was cleared, lookup must perform a fresh login
      # before the callsign request. Expect exactly two requests.
      Req.Test.expect(Client, 2, fn conn ->
        params = Plug.Conn.fetch_query_params(conn).query_params

        cond do
          Map.has_key?(params, "username") ->
            Plug.Conn.send_resp(conn, 200, @session_xml)

          Map.has_key?(params, "callsign") ->
            Plug.Conn.send_resp(conn, 200, @callsign_xml)
        end
      end)

      assert {:ok, %{"call" => "W1AW"}} = Client.lookup("W1AW")
    end
  end

  describe "XML parsing edge cases" do
    test "login on malformed XML crashes (xmerl is strict)" do
      Req.Test.stub(Client, fn conn ->
        Plug.Conn.send_resp(conn, 200, "not xml at all")
      end)

      # xmerl_scan.string/1 exits on unparseable input. The current client
      # does not guard this, so the caller sees an exit rather than an
      # {:error, _} tuple.
      assert catch_exit(Client.login())
    end

    test "login returns {:error, _} on empty Session element" do
      Req.Test.stub(Client, fn conn ->
        Plug.Conn.send_resp(conn, 200, """
        <?xml version="1.0" encoding="utf-8"?>
        <QRZDatabase version="1.34">
          <Session/>
        </QRZDatabase>
        """)
      end)

      assert {:error, "Unknown error"} = Client.login()
    end
  end
end
