defmodule AprsmeWeb.Plugs.ContentSecurityPolicyTest do
  use AprsmeWeb.ConnCase

  alias AprsmeWeb.Plugs.ContentSecurityPolicy

  describe "call/2" do
    test "sets Content-Security-Policy header", %{conn: conn} do
      conn = ContentSecurityPolicy.call(conn, [])

      csp = conn |> get_resp_header("content-security-policy") |> List.first()
      assert is_binary(csp) and byte_size(csp) > 0

      # Verify required directives are present
      assert csp =~ "default-src 'self'"
      assert csp =~ "object-src 'none'"
      assert csp =~ "frame-ancestors 'none'"
      assert csp =~ "base-uri 'self'"
      assert csp =~ "form-action 'self'"
    end

    test "includes nonce in script-src", %{conn: conn} do
      conn = ContentSecurityPolicy.call(conn, [])

      csp = conn |> get_resp_header("content-security-policy") |> List.first()

      # Verify nonce-based script-src is used instead of unsafe-inline
      assert csp =~ ~r/script-src[^;]*'nonce-[A-Za-z0-9+\/=]+'/

      # Extract just the script-src directive and ensure it has no unsafe-inline
      [script_src] = Regex.run(~r/script-src\s+([^;]+)/, csp, capture: :all_but_first)
      refute script_src =~ "'unsafe-inline'"
    end

    test "stores nonce in conn.private", %{conn: conn} do
      conn = ContentSecurityPolicy.call(conn, [])

      nonce = conn.private[:csp_nonce]
      assert is_binary(nonce) and byte_size(nonce) > 0

      # Verify the private nonce matches the one in the header
      csp = conn |> get_resp_header("content-security-policy") |> List.first()
      assert csp =~ "'nonce-#{nonce}'"
    end

    test "generates a unique nonce per request", %{conn: conn} do
      conn1 = ContentSecurityPolicy.call(conn, [])
      conn2 = ContentSecurityPolicy.call(build_conn(), [])

      nonce1 = conn1.private[:csp_nonce]
      nonce2 = conn2.private[:csp_nonce]

      assert nonce1 != nonce2
    end

    test "allows external script sources", %{conn: conn} do
      conn = ContentSecurityPolicy.call(conn, [])

      csp = conn |> get_resp_header("content-security-policy") |> List.first()

      # Plausible analytics (self-hosted)
      assert csp =~ "script-src" and csp =~ "https://a.w5isp.com"
      # Sentry error tracking
      assert csp =~ "script-src" and csp =~ "https://js.sentry-cdn.com"
      # Leaflet/map CDN
      assert csp =~ "script-src" and csp =~ "https://unpkg.com"
    end

    test "allows map tile connect sources", %{conn: conn} do
      conn = ContentSecurityPolicy.call(conn, [])

      csp = conn |> get_resp_header("content-security-policy") |> List.first()

      assert csp =~ "connect-src" and csp =~ "https://tile.openstreetmap.org"
      assert csp =~ "connect-src" and csp =~ "https://*.tile.openstreetmap.org"
      assert csp =~ "connect-src" and csp =~ "https://*.basemaps.cartocdn.com"
      assert csp =~ "connect-src" and csp =~ "wss:"
    end

    test "allows sentry connect sources", %{conn: conn} do
      conn = ContentSecurityPolicy.call(conn, [])

      csp = conn |> get_resp_header("content-security-policy") |> List.first()

      assert csp =~ "connect-src" and csp =~ "https://*.ingest.sentry.io"
      assert csp =~ "connect-src" and csp =~ "https://*.sentry.io"
    end

    test "allows image sources from any HTTPS origin for map tiles", %{conn: conn} do
      conn = ContentSecurityPolicy.call(conn, [])

      csp = conn |> get_resp_header("content-security-policy") |> List.first()

      assert csp =~ "img-src" and csp =~ "https:"
      assert csp =~ "img-src" and csp =~ "data:"
      assert csp =~ "img-src" and csp =~ "blob:"
    end

    test "allows unsafe-inline for style-src (required by Tailwind and Phoenix)", %{conn: conn} do
      conn = ContentSecurityPolicy.call(conn, [])

      csp = conn |> get_resp_header("content-security-policy") |> List.first()

      # style-src still needs unsafe-inline for Tailwind/Phoenix inline styles
      assert csp =~ "style-src" and csp =~ "'unsafe-inline'"
    end

    test "sets worker-src to allow web workers and blob workers", %{conn: conn} do
      conn = ContentSecurityPolicy.call(conn, [])

      csp = conn |> get_resp_header("content-security-policy") |> List.first()

      assert csp =~ "worker-src 'self' blob:"
    end

    test "blocks framing via frame-ancestors 'none'", %{conn: conn} do
      conn = ContentSecurityPolicy.call(conn, [])

      csp = conn |> get_resp_header("content-security-policy") |> List.first()

      assert csp =~ "frame-ancestors 'none'"
    end
  end
end
