"""Tests for RIA onboarding v2 routes and related service helpers."""

from __future__ import annotations

import asyncio
import json
import sys
import types
from datetime import datetime, timedelta, timezone
from typing import Any

import httpx
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

# ---------------------------------------------------------------------------
# Stub the rate-limit middleware *before* importing the route module so the
# decorator is a no-op during tests.
# ---------------------------------------------------------------------------
rate_limit_module = types.ModuleType("api.middlewares.rate_limit")


class _NoopLimiter:
    def limit(self, *_args, **_kwargs):
        def decorator(func):
            return func

        return decorator


rate_limit_module.limiter = _NoopLimiter()
sys.modules.setdefault("api.middlewares.rate_limit", rate_limit_module)

import hushh_mcp.services.ria_iam_service as ria_iam_service_module  # noqa: E402
from api.middleware import require_firebase_auth  # noqa: E402
from api.routes import ria as ria_module  # noqa: E402
from hushh_mcp.services.crd_scrape_proxy_service import (  # noqa: E402
    CrdScrapeProviderResponse,
    CrdScrapeProxyService,
)
from hushh_mcp.services.ria_iam_service import (  # noqa: E402
    RIAIAMPolicyError,
    RIAIAMService,
    _brokercheck_branch_location_from_text,
    _firm_location_from_text,
    _official_location_from_text,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_TEST_UID = "user_test_123"


def _build_app() -> FastAPI:
    app = FastAPI()
    app.include_router(ria_module.router)
    return app


def _authed_app() -> FastAPI:
    """Return an app with Firebase auth overridden to return _TEST_UID."""
    app = _build_app()
    app.dependency_overrides[require_firebase_auth] = lambda: _TEST_UID
    return app


class _FakeAcquire:
    def __init__(self, conn: Any) -> None:
        self._conn = conn

    async def __aenter__(self):
        return self._conn

    async def __aexit__(self, exc_type, exc, tb):
        return False


class _FakePool:
    def __init__(self, conn: Any) -> None:
        self._conn = conn

    def acquire(self):
        return _FakeAcquire(self._conn)


# ===================================================================
# Route: POST /api/ria/onboarding/verify-license
# ===================================================================


def test_verify_license_found(monkeypatch) -> None:
    """Happy path: broker intelligence returns a verified advisor."""

    async def _mock_verify(
        self,
        user_id,
        *,
        license_number,
        regulator=None,
        force_live_verification=False,
    ):
        assert user_id == _TEST_UID
        assert license_number == "7413463"
        assert force_live_verification is False
        return {
            "status": "found",
            "advisor_name": "Andrew Garrett Kirkland",
            "firm_name": "Renaissance Advisory Group",
            "crd_number": "7413463",
            "regulator": regulator or "SEC",
            "scrape_job_id": "crd_scrape_abc123",
            "provider": "ria_intelligence_combined",
        }

    monkeypatch.setattr(RIAIAMService, "verify_ria_license", _mock_verify)

    client = TestClient(_authed_app())
    response = client.post(
        "/api/ria/onboarding/verify-license",
        json={"license_number": "7413463"},
    )

    assert response.status_code == 200
    payload = response.json()
    assert payload["status"] == "found"
    assert payload["advisor_name"] == "Andrew Garrett Kirkland"
    assert payload["firm_name"] == "Renaissance Advisory Group"
    assert payload["crd_number"] == "7413463"
    assert payload["regulator"] == "SEC"
    assert payload["scrape_job_id"] == "crd_scrape_abc123"


def test_verify_license_not_found(monkeypatch) -> None:
    """Broker intelligence finds no matching advisor for the given license."""

    async def _mock_verify(
        self,
        user_id,
        *,
        license_number,
        regulator=None,
        force_live_verification=False,
    ):
        assert force_live_verification is False
        return {
            "status": "not_found",
            "advisor_name": None,
            "firm_name": None,
            "crd_number": license_number,
            "regulator": None,
            "scrape_job_id": None,
            "provider": "ria_intelligence_combined",
        }

    monkeypatch.setattr(RIAIAMService, "verify_ria_license", _mock_verify)

    client = TestClient(_authed_app())
    response = client.post(
        "/api/ria/onboarding/verify-license",
        json={"license_number": "0000000"},
    )

    assert response.status_code == 200
    assert response.json()["status"] == "not_found"


def test_verify_license_passes_force_live_verification(monkeypatch) -> None:
    captured: dict[str, Any] = {}

    async def _mock_verify(
        self,
        user_id,
        *,
        license_number,
        regulator=None,
        force_live_verification=False,
    ):
        captured.update(
            {
                "user_id": user_id,
                "license_number": license_number,
                "regulator": regulator,
                "force_live_verification": force_live_verification,
            }
        )
        return {
            "status": "found",
            "advisor_name": "Andrew Garrett Kirkland",
            "firm_name": "Renaissance Advisory Group",
            "crd_number": license_number,
            "regulator": regulator,
            "provider": "ria_intelligence_combined",
        }

    monkeypatch.setattr(RIAIAMService, "verify_ria_license", _mock_verify)

    client = TestClient(_authed_app())
    response = client.post(
        "/api/ria/onboarding/verify-license",
        json={
            "license_number": "7413463",
            "regulator": "SEC",
            "force_live_verification": True,
        },
    )

    assert response.status_code == 200
    assert captured == {
        "user_id": _TEST_UID,
        "license_number": "7413463",
        "regulator": "SEC",
        "force_live_verification": True,
    }


def test_verify_ria_license_returns_recent_cache_without_provider(monkeypatch) -> None:
    cached_at = datetime.now(timezone.utc) - timedelta(minutes=5)
    cached_payload = {
        "verifiedName": "Andrew Garrett Kirkland",
        "currentFirm": "Eissman Wealth Management",
        "status": "Investment Adviser Representative",
        "crdNumber": "7413463",
        "city": "Kennesaw",
        "state": "GA",
        "pinZip": "30144",
        "fullStreetAddress": "114 Townpark Drive, Ste. 175",
        "exams": ["Series 65"],
        "disclosures": {"count": 1},
        "employmentHistory": [],
        "summary": "Cached official source-backed profile.",
    }

    class _FakeConn:
        async def fetchrow(self, query: str, *args):
            assert "status = 'completed'" in query
            assert "created_at >= $3" in query
            assert args[0] == _TEST_UID
            assert args[1] == "7413463"
            assert args[2] > datetime.now(timezone.utc) - timedelta(hours=25)
            return {
                "raw_response": json.dumps(cached_payload),
                "regulator": "SEC",
                "created_at": cached_at,
            }

        async def execute(self, *_args):
            raise AssertionError("cache hit must not insert a new audit row")

    async def _fake_get_pool():
        return _FakePool(_FakeConn())

    class _UnexpectedProxy:
        async def broker_intelligence(self, **_kwargs):
            raise AssertionError("cache hit must not call broker intelligence")

        async def create_job(self, **_kwargs):
            raise AssertionError("cache hit must not create a scrape job")

    monkeypatch.setattr(ria_iam_service_module, "get_pool", _fake_get_pool)
    monkeypatch.setattr(
        "hushh_mcp.services.crd_scrape_proxy_service.CrdScrapeProxyService",
        lambda: _UnexpectedProxy(),
    )

    result = asyncio.run(
        RIAIAMService().verify_ria_license(
            _TEST_UID,
            license_number="7413463",
            regulator=None,
        )
    )

    assert result["status"] == "found"
    assert result["advisor_name"] == "Andrew Garrett Kirkland"
    assert result["firm_name"] == "Eissman Wealth Management"
    assert result["city"] == "Kennesaw"
    assert result["pin_zip"] == "30144"
    assert result["full_street_address"] == "114 Townpark Drive, Ste. 175"
    assert result["cache_hit"] is True
    assert result["cached_at"].endswith("Z")
    assert result["cache_expires_at"].endswith("Z")
    assert result["cache_ttl_seconds"] == 24 * 60 * 60


def test_verify_ria_license_ignores_incomplete_location_cache(monkeypatch) -> None:
    cached_at = datetime.now(timezone.utc) - timedelta(minutes=5)
    cached_payload = {
        "verifiedName": "Andrew Garrett Kirkland",
        "currentFirm": "Eissman Wealth Management",
        "status": "Investment Adviser Representative",
        "crdNumber": "7413463",
        "exams": ["Series 66"],
        "summary": "Cached profile without a source-backed business location.",
    }
    calls = {"broker": 0, "scrape": 0, "execute": 0}
    captured_audit: dict[str, Any] = {}

    class _FakeConn:
        async def fetchrow(self, query: str, *args):
            assert "status = 'completed'" in query
            assert args[0] == _TEST_UID
            assert args[1] == "7413463"
            return {
                "raw_response": json.dumps(cached_payload),
                "regulator": "SEC",
                "created_at": cached_at,
            }

        async def execute(self, _query, *args):
            calls["execute"] += 1
            captured_audit["payload"] = json.loads(args[4])
            captured_audit["status"] = args[5]
            return "INSERT 0 1"

    async def _fake_get_pool():
        return _FakePool(_FakeConn())

    class _FakeProxy:
        async def broker_intelligence(self, *, query: str, request_id: str | None = None):
            _ = request_id
            calls["broker"] += 1
            assert query == "7413463"
            return CrdScrapeProviderResponse(
                200,
                {
                    "verifiedName": "Andrew Garrett Kirkland",
                    "currentFirm": "Eissman Wealth Management",
                    "status": "Investment Adviser Representative",
                    "crdNumber": "7413463",
                    "exams": ["Series 66"],
                },
            )

        async def create_job(self, *, crd_number: str, request_id: str | None = None):
            _ = request_id
            calls["scrape"] += 1
            assert crd_number == "7413463"
            return CrdScrapeProviderResponse(202, {"jobId": "crd_scrape_location_refresh"})

    async def _mock_official_location(crd_number: str):
        assert crd_number == "7413463"
        return {
            "city": "Kennesaw",
            "state": "GA",
            "pin_zip": "30144",
            "address": "114 Townpark Drive, Ste. 175",
            "location": "Kennesaw, GA",
            "source_url": "https://reports.adviserinfo.sec.gov/reports/individual/individual_7413463.pdf",
        }

    monkeypatch.setattr(ria_iam_service_module, "get_pool", _fake_get_pool)
    monkeypatch.setattr(
        "hushh_mcp.services.crd_scrape_proxy_service.CrdScrapeProxyService",
        lambda: _FakeProxy(),
    )
    monkeypatch.setattr(
        "hushh_mcp.services.ria_iam_service._official_pdf_location_for_crd",
        _mock_official_location,
    )

    result = asyncio.run(
        RIAIAMService().verify_ria_license(
            _TEST_UID,
            license_number="7413463",
            regulator="SEC",
        )
    )

    assert result["status"] == "found"
    assert result["cache_hit"] is False
    assert result["city"] == "Kennesaw"
    assert result["pin_zip"] == "30144"
    assert result["full_street_address"] == "114 Townpark Drive, Ste. 175"
    assert calls == {"broker": 1, "scrape": 1, "execute": 1}
    assert captured_audit["status"] == "completed"
    assert captured_audit["payload"]["city"] == "Kennesaw"
    assert captured_audit["payload"]["pin_zip"] == "30144"
    assert captured_audit["payload"]["full_street_address"] == "114 Townpark Drive, Ste. 175"
    assert captured_audit["payload"]["official_location"]["source_url"].endswith(
        "individual_7413463.pdf"
    )


def test_verify_ria_license_force_live_bypasses_recent_cache(monkeypatch) -> None:
    calls = {"broker": 0, "scrape": 0, "fetchrow": 0, "execute": 0}

    class _FakeConn:
        async def fetchrow(self, *_args):
            calls["fetchrow"] += 1
            raise AssertionError("force_live_verification must skip cache lookup")

        async def execute(self, *_args):
            calls["execute"] += 1
            return "INSERT 0 1"

    async def _fake_get_pool():
        return _FakePool(_FakeConn())

    class _FakeProxy:
        async def broker_intelligence(self, *, query: str, request_id: str | None = None):
            _ = request_id
            calls["broker"] += 1
            assert query == "7413463"
            return CrdScrapeProviderResponse(
                200,
                {
                    "verifiedName": "Andrew Garrett Kirkland",
                    "currentFirm": "Eissman Wealth Management",
                    "status": "Investment Adviser Representative",
                    "crdNumber": "7413463",
                    "exams": ["Series 65"],
                },
            )

        async def create_job(self, *, crd_number: str, request_id: str | None = None):
            _ = request_id
            calls["scrape"] += 1
            assert crd_number == "7413463"
            return CrdScrapeProviderResponse(202, {"jobId": "crd_scrape_live"})

    monkeypatch.setattr(ria_iam_service_module, "get_pool", _fake_get_pool)
    monkeypatch.setattr(
        "hushh_mcp.services.crd_scrape_proxy_service.CrdScrapeProxyService",
        lambda: _FakeProxy(),
    )

    result = asyncio.run(
        RIAIAMService().verify_ria_license(
            _TEST_UID,
            license_number="7413463",
            regulator="SEC",
            force_live_verification=True,
        )
    )

    assert result["status"] == "found"
    assert result["cache_hit"] is False
    assert result["scrape_job_id"] == "crd_scrape_live"
    assert calls == {"broker": 1, "scrape": 1, "fetchrow": 0, "execute": 1}


def test_verify_ria_license_cache_miss_uses_completed_fresh_cutoff(monkeypatch) -> None:
    calls = {"broker": 0, "scrape": 0, "execute": 0}

    class _FakeConn:
        async def fetchrow(self, query: str, *args):
            assert "status = 'completed'" in query
            assert "created_at >= $3" in query
            assert args[2] > datetime.now(timezone.utc) - timedelta(hours=25)
            return None

        async def execute(self, *_args):
            calls["execute"] += 1
            return "INSERT 0 1"

    async def _fake_get_pool():
        return _FakePool(_FakeConn())

    class _FakeProxy:
        async def broker_intelligence(self, *, query: str, request_id: str | None = None):
            _ = request_id
            calls["broker"] += 1
            assert query == "7413463"
            return CrdScrapeProviderResponse(
                200,
                {
                    "verifiedName": "Andrew Garrett Kirkland",
                    "status": "Investment Adviser Representative",
                    "crdNumber": "7413463",
                },
            )

        async def create_job(self, *, crd_number: str, request_id: str | None = None):
            _ = request_id
            calls["scrape"] += 1
            assert crd_number == "7413463"
            return CrdScrapeProviderResponse(202, {"jobId": "crd_scrape_miss"})

    monkeypatch.setattr(ria_iam_service_module, "get_pool", _fake_get_pool)
    monkeypatch.setattr(
        "hushh_mcp.services.crd_scrape_proxy_service.CrdScrapeProxyService",
        lambda: _FakeProxy(),
    )

    result = asyncio.run(
        RIAIAMService().verify_ria_license(
            _TEST_UID,
            license_number="7413463",
            regulator="SEC",
        )
    )

    assert result["status"] == "found"
    assert result["cache_hit"] is False
    assert result["scrape_job_id"] == "crd_scrape_miss"
    assert calls == {"broker": 1, "scrape": 1, "execute": 1}


def test_verify_license_passes_through_city_pin_zip_and_string_exams(monkeypatch) -> None:
    """The service should preserve source-backed location fields from broker intelligence."""

    class _FakeProxy:
        async def broker_intelligence(self, *, query: str, request_id: str | None = None):
            assert query == "7413463"
            return CrdScrapeProviderResponse(
                200,
                {
                    "verifiedName": "Andrew Garrett Kirkland",
                    "currentFirm": "Eissman Wealth Management",
                    "status": "Investment Adviser Representative",
                    "crdNumber": "7413463",
                    "city": "Kennesaw",
                    "pinZip": "30144",
                    "exams": ["Series 65"],
                    "disclosures": {"count": 1},
                    "employmentHistory": [],
                    "summary": "Official PDF-backed location resolved.",
                },
            )

        async def create_job(self, *, crd_number: str, request_id: str | None = None):
            assert crd_number == "7413463"
            return CrdScrapeProviderResponse(
                202,
                {"jobId": "crd_scrape_location_123", "status": "queued"},
            )

    monkeypatch.setattr(
        "hushh_mcp.services.crd_scrape_proxy_service.CrdScrapeProxyService",
        lambda: _FakeProxy(),
    )

    async def _mock_official_location(crd_number: str):
        assert crd_number == "7413463"
        return {
            "city": "Kennesaw",
            "state": "GA",
            "pin_zip": "30144",
            "address": "114 Townpark Drive, Ste. 175",
            "location": "Kennesaw, GA",
            "source_url": "https://reports.adviserinfo.sec.gov/reports/individual/individual_7413463.pdf",
        }

    monkeypatch.setattr(
        "hushh_mcp.services.ria_iam_service._official_pdf_location_for_crd",
        _mock_official_location,
    )

    result = asyncio.run(
        RIAIAMService().verify_ria_license(
            _TEST_UID,
            license_number="7413463",
            regulator="SEC",
        )
    )

    assert result["status"] == "found"
    assert result["city"] == "Kennesaw"
    assert result["pin_zip"] == "30144"
    assert result["state"] == "GA"
    assert result["area_locality"] == "GA"
    assert result["full_street_address"] == "114 Townpark Drive, Ste. 175"
    assert result["official_location"]["source_url"].endswith("individual_7413463.pdf")
    assert result["certifications"] == ["Series 65"]
    assert result["exams_passed"] == ["Series 65"]


def test_verify_license_fills_missing_location_from_official_pdf(monkeypatch) -> None:
    """When provider location is blank, use official regulator PDF text as fallback."""

    class _FakeProxy:
        async def broker_intelligence(self, *, query: str, request_id: str | None = None):
            assert query == "7413463"
            return CrdScrapeProviderResponse(
                200,
                {
                    "verifiedName": "Andrew Garrett Kirkland",
                    "currentFirm": "Not Currently Registered",
                    "status": "Investment Adviser Representative",
                    "crdNumber": "7413463",
                    "city": None,
                    "pinZip": None,
                    "exams": ["Series 66"],
                    "employmentHistory": [],
                },
            )

        async def create_job(self, *, crd_number: str, request_id: str | None = None):
            assert crd_number == "7413463"
            return CrdScrapeProviderResponse(404, {"detail": "Not Found"})

    async def _mock_official_location(crd_number: str):
        assert crd_number == "7413463"
        return {
            "city": "Kennesaw",
            "state": "GA",
            "pin_zip": "30144",
            "address": "114 Townpark Drive, Ste. 175",
            "location": "Kennesaw, GA",
            "source_url": "https://reports.adviserinfo.sec.gov/reports/individual/individual_7413463.pdf",
        }

    monkeypatch.setattr(
        "hushh_mcp.services.crd_scrape_proxy_service.CrdScrapeProxyService",
        lambda: _FakeProxy(),
    )
    monkeypatch.setattr(
        "hushh_mcp.services.ria_iam_service._official_pdf_location_for_crd",
        _mock_official_location,
    )

    result = asyncio.run(
        RIAIAMService().verify_ria_license(
            _TEST_UID,
            license_number="7413463",
            regulator="SEC",
        )
    )

    assert result["status"] == "found"
    assert result["city"] == "Kennesaw"
    assert result["pin_zip"] == "30144"
    assert result["state"] == "GA"
    assert result["area_locality"] == "GA"
    assert result["full_street_address"] == "114 Townpark Drive, Ste. 175"
    assert result["certifications"] == ["Series 66"]
    assert result["exams_passed"] == ["Series 66"]


def test_verify_license_falls_back_to_official_pdf_when_provider_5xx(monkeypatch) -> None:
    """A transient broker-intelligence 5xx should not become an empty not_found result."""
    captured_audit: dict[str, Any] = {}

    class _FakeProxy:
        async def broker_intelligence(self, *, query: str, request_id: str | None = None):
            assert query == "7413463"
            return CrdScrapeProviderResponse(502, {"error": "Bad Gateway"})

        async def create_job(self, *, crd_number: str, request_id: str | None = None):
            assert crd_number == "7413463"
            return CrdScrapeProviderResponse(404, {"detail": "Not Found"})

    async def _mock_official_profile(crd_number: str):
        assert crd_number == "7413463"
        return {
            "advisor_name": "Andrew Garrett Kirkland",
            "crd_number": "7413463",
            "firm_name": "Eissman Wealth Management",
            "regulator_status": "Not currently registered as an Investment Adviser Representative",
            "certifications": ["Series 66 - Uniform Combined State Law Examination"],
            "summary": "Official IAPD records list Andrew Garrett Kirkland under CRD 7413463.",
            "official_location": {
                "city": "Kennesaw",
                "state": "GA",
                "pin_zip": "30144",
                "address": "114 Townpark Drive, Ste. 175",
                "location": "Kennesaw, GA",
                "source_url": (
                    "https://reports.adviserinfo.sec.gov/reports/individual/individual_7413463.pdf"
                ),
            },
            "source_url": (
                "https://reports.adviserinfo.sec.gov/reports/individual/individual_7413463.pdf"
            ),
        }

    monkeypatch.setattr(
        "hushh_mcp.services.crd_scrape_proxy_service.CrdScrapeProxyService",
        lambda: _FakeProxy(),
    )
    monkeypatch.setattr(
        "hushh_mcp.services.ria_iam_service._official_pdf_profile_for_crd",
        _mock_official_profile,
    )

    class _FakeConn:
        async def fetchrow(self, *_args):
            return None

        async def execute(self, _query, *args):
            captured_audit["payload"] = json.loads(args[4])
            captured_audit["status"] = args[5]
            return "INSERT 0 1"

    async def _fake_get_pool():
        return _FakePool(_FakeConn())

    monkeypatch.setattr(ria_iam_service_module, "get_pool", _fake_get_pool)

    result = asyncio.run(
        RIAIAMService().verify_ria_license(
            _TEST_UID,
            license_number="7413463",
            regulator="SEC",
        )
    )

    assert result["status"] == "found"
    assert result["advisor_name"] == "Andrew Garrett Kirkland"
    assert result["firm_name"] == "Eissman Wealth Management"
    assert result["city"] == "Kennesaw"
    assert result["pin_zip"] == "30144"
    assert result["full_street_address"] == "114 Townpark Drive, Ste. 175"
    assert result["certifications"] == ["Series 66 - Uniform Combined State Law Examination"]
    assert result["exams_passed"] == result["certifications"]
    assert result["broker_intelligence_summary"].startswith("Official IAPD records")
    assert captured_audit["status"] == "completed"
    assert captured_audit["payload"]["official_profile"]["advisor_name"] == (
        "Andrew Garrett Kirkland"
    )


def test_official_location_from_text_extracts_city_and_zip() -> None:
    """The fallback parser extracts address fields from official report text."""

    result = _official_location_from_text(
        "Business Address 114 Townpark Drive, Ste. 175 Kennesaw, GA 30144 Registered Since 09/2021",
        "https://reports.adviserinfo.sec.gov/reports/individual/individual_7413463.pdf",
    )

    assert result == {
        "city": "Kennesaw",
        "state": "GA",
        "pin_zip": "30144",
        "address": "114 Townpark Drive, Ste. 175",
        "location": "Kennesaw, GA",
        "source_url": "https://reports.adviserinfo.sec.gov/reports/individual/individual_7413463.pdf",
    }


def test_brokercheck_branch_location_from_text_extracts_inactive_broker_city() -> None:
    """Inactive broker reports expose branch city/state even without a full address."""

    result = _brokercheck_branch_location_from_text(
        (
            "Registration Dates Firm Name CRD# Branch Location "
            "B 09/2021 - 06/2022 LAZARD FRERES & CO. LLC 2528 NEW YORK, NY "
            "Employment History"
        ),
        "https://files.brokercheck.finra.org/individual/individual_7265726.pdf",
    )

    assert result == {
        "city": "New York",
        "state": "NY",
        "location": "New York, NY",
        "firm_crd": "2528",
        "firm_name": "LAZARD FRERES & CO. LLC",
        "source_url": "https://files.brokercheck.finra.org/individual/individual_7265726.pdf",
    }


def test_firm_location_from_text_extracts_main_office_zip() -> None:
    """Firm BrokerCheck PDFs can supply the ZIP missing from inactive individual reports."""

    result = _firm_location_from_text(
        "\n".join(
            [
                "Main Office Location Firm Profile Disclosure Events",
                "30 ROCKEFELLER PLAZA This firm is classified as a limited liability company.",
                "NEW YORK, NY 10020-5900",
                "Mailing Address proceedings and financial matters",
            ]
        ),
        "https://files.brokercheck.finra.org/firm/firm_2528.pdf",
    )

    assert result == {
        "city": "New York",
        "state": "NY",
        "pin_zip": "10020-5900",
        "address": "30 ROCKEFELLER PLAZA",
        "location": "New York, NY",
        "source_url": "https://files.brokercheck.finra.org/firm/firm_2528.pdf",
    }


def test_verify_license_invalid_number_422() -> None:
    """An empty license_number should be rejected by Pydantic validation (422)."""
    client = TestClient(_authed_app())
    response = client.post(
        "/api/ria/onboarding/verify-license",
        json={"license_number": ""},
    )

    assert response.status_code == 422


def test_verify_license_requires_auth() -> None:
    """Without auth override the endpoint must reject with 401."""
    app = _build_app()  # No dependency override
    client = TestClient(app, raise_server_exceptions=False)
    response = client.post(
        "/api/ria/onboarding/verify-license",
        json={"license_number": "7413463"},
    )

    assert response.status_code == 401


def test_verify_license_partial_data(monkeypatch) -> None:
    """Broker intelligence returns advisor name only, no CRD number."""

    async def _mock_verify(
        self,
        user_id,
        *,
        license_number,
        regulator=None,
        force_live_verification=False,
    ):
        assert force_live_verification is False
        return {
            "status": "found",
            "advisor_name": "Jane Doe",
            "firm_name": None,
            "crd_number": None,
            "regulator": None,
            "scrape_job_id": None,
            "provider": "ria_intelligence_combined",
        }

    monkeypatch.setattr(RIAIAMService, "verify_ria_license", _mock_verify)

    client = TestClient(_authed_app())
    response = client.post(
        "/api/ria/onboarding/verify-license",
        json={"license_number": "9999999"},
    )

    assert response.status_code == 200
    payload = response.json()
    assert payload["advisor_name"] == "Jane Doe"
    assert payload["crd_number"] is None
    assert payload["firm_name"] is None


def test_verify_ria_license_exposes_summary_and_exams_as_prefill(monkeypatch) -> None:
    """The service should not discard source-backed bio and qualification data."""

    async def _mock_broker_intelligence(self, *, query, request_id=None):
        assert query == "7413463"
        return CrdScrapeProviderResponse(
            status_code=200,
            payload={
                "status": "Investment Adviser Representative",
                "verifiedName": "Andrew Garrett Kirkland",
                "currentFirm": "Eissman Wealth Management",
                "crdNumber": "7413463",
                "city": "Kennesaw",
                "pinZip": "30144",
                "summary": (
                    "Andrew Garrett Kirkland is a financial professional "
                    "serving clients at Eissman Wealth Management."
                ),
                "exams": [
                    "Securities Industry Essentials Examination (SIE)",
                    "Uniform Combined State Law Examination (Series 66)",
                ],
            },
        )

    async def _mock_create_job(self, *, crd_number, request_id=None):
        assert crd_number == "7413463"
        return CrdScrapeProviderResponse(
            status_code=202,
            payload={"jobId": "crd_scrape_prefill", "status": "queued"},
        )

    async def _mock_get_pool():
        raise RuntimeError("no database in unit test")

    async def _mock_official_location(crd_number: str):
        assert crd_number == "7413463"
        return None

    monkeypatch.setattr(CrdScrapeProxyService, "broker_intelligence", _mock_broker_intelligence)
    monkeypatch.setattr(CrdScrapeProxyService, "create_job", _mock_create_job)
    monkeypatch.setattr(
        ria_iam_service_module,
        "_official_pdf_location_for_crd",
        _mock_official_location,
    )
    monkeypatch.setattr(ria_iam_service_module, "get_pool", _mock_get_pool)

    payload = asyncio.run(
        RIAIAMService().verify_ria_license(
            _TEST_UID,
            license_number="7413463",
            regulator=None,
        )
    )

    assert payload["status"] == "found"
    assert payload["advisor_name"] == "Andrew Garrett Kirkland"
    assert payload["bio"].startswith("Andrew Garrett Kirkland is a financial professional")
    assert payload["certifications"] == [
        "Securities Industry Essentials Examination (SIE)",
        "Uniform Combined State Law Examination (Series 66)",
    ]
    assert payload["exams_passed"] == payload["certifications"]
    assert payload["scrape_job_id"] == "crd_scrape_prefill"


def test_verify_ria_license_fills_inactive_broker_location_from_official_fallback(
    monkeypatch,
) -> None:
    """Inactive BrokerCheck rows should still prefill city, state, ZIP, and address."""

    captured_audit: dict[str, Any] = {}

    async def _mock_broker_intelligence(self, *, query, request_id=None):
        assert query == "7265726"
        return CrdScrapeProviderResponse(
            status_code=200,
            payload={
                "status": "Not Currently Registered",
                "verifiedName": "Ria Ashley Sen",
                "currentFirm": "Not Currently Registered",
                "crdNumber": "7265726",
                "summary": "Former registered representative.",
                "exams": [
                    "Securities Industry Essentials Examination (SIE)",
                    "Investment Banking Representative Examination (Series 79TO)",
                ],
            },
        )

    async def _mock_create_job(self, *, crd_number, request_id=None):
        assert crd_number == "7265726"
        return CrdScrapeProviderResponse(
            status_code=202,
            payload={"jobId": "crd_scrape_inactive", "status": "queued"},
        )

    async def _mock_official_location(crd_number: str):
        assert crd_number == "7265726"
        return {
            "city": "New York",
            "state": "NY",
            "pin_zip": "10020-5900",
            "address": "30 ROCKEFELLER PLAZA",
            "location": "New York, NY",
            "source_url": "https://files.brokercheck.finra.org/firm/firm_2528.pdf",
            "individual_source_url": (
                "https://files.brokercheck.finra.org/individual/individual_7265726.pdf"
            ),
            "firm_crd": "2528",
            "firm_name": "LAZARD FRERES & CO. LLC",
        }

    class _FakeConn:
        async def fetchrow(self, *_args):
            return None

        async def execute(self, _query: str, *args):
            captured_audit["payload"] = json.loads(args[4])
            captured_audit["status"] = args[5]

    async def _mock_get_pool():
        return _FakePool(_FakeConn())

    monkeypatch.setattr(CrdScrapeProxyService, "broker_intelligence", _mock_broker_intelligence)
    monkeypatch.setattr(CrdScrapeProxyService, "create_job", _mock_create_job)
    monkeypatch.setattr(
        ria_iam_service_module,
        "_official_pdf_location_for_crd",
        _mock_official_location,
    )
    monkeypatch.setattr(ria_iam_service_module, "get_pool", _mock_get_pool)

    payload = asyncio.run(
        RIAIAMService().verify_ria_license(
            _TEST_UID,
            license_number="7265726",
            regulator="SEC",
        )
    )

    assert payload["status"] == "found"
    assert payload["advisor_name"] == "Ria Ashley Sen"
    assert payload["firm_name"] == "Not Currently Registered"
    assert payload["city"] == "New York"
    assert payload["state"] == "NY"
    assert payload["pin_zip"] == "10020-5900"
    assert payload["full_street_address"] == "30 ROCKEFELLER PLAZA"
    assert payload["business_address"] == "30 ROCKEFELLER PLAZA"
    assert payload["official_location"]["individual_source_url"].endswith("individual_7265726.pdf")
    assert payload["scrape_job_id"] == "crd_scrape_inactive"
    assert captured_audit["status"] == "completed"
    assert captured_audit["payload"]["city"] == "New York"
    assert captured_audit["payload"]["pin_zip"] == "10020-5900"


def test_verify_license_provider_timeout(monkeypatch) -> None:
    """Service raises RIAIAMPolicyError(status_code=503) on provider timeout."""

    async def _mock_verify(
        self,
        user_id,
        *,
        license_number,
        regulator=None,
        force_live_verification=False,
    ):
        assert force_live_verification is False
        raise RIAIAMPolicyError("Provider timed out", status_code=503)

    monkeypatch.setattr(RIAIAMService, "verify_ria_license", _mock_verify)

    client = TestClient(_authed_app(), raise_server_exceptions=False)
    response = client.post(
        "/api/ria/onboarding/verify-license",
        json={"license_number": "7413463"},
    )

    assert response.status_code == 503


# ===================================================================
# Route: POST /api/ria/profile/refresh-license
# ===================================================================


def test_refresh_license_profile_route_maps_payload(monkeypatch) -> None:
    captured: dict[str, Any] = {}

    async def _mock_refresh(
        self,
        user_id,
        *,
        license_number,
        regulator=None,
        force_live_verification=False,
    ):
        captured.update(
            {
                "user_id": user_id,
                "license_number": license_number,
                "regulator": regulator,
                "force_live_verification": force_live_verification,
            }
        )
        return {
            "updated": True,
            "status": "found",
            "message": "Official RIA data updated.",
            "applied_fields": ["finra_crd"],
        }

    monkeypatch.setattr(RIAIAMService, "refresh_ria_profile_from_license", _mock_refresh)

    client = TestClient(_authed_app())
    response = client.post(
        "/api/ria/profile/refresh-license",
        json={
            "license_number": "7413463",
            "regulator": "SEC",
            "force_live_verification": True,
        },
    )

    assert response.status_code == 200
    assert response.json()["updated"] is True
    assert captured == {
        "user_id": _TEST_UID,
        "license_number": "7413463",
        "regulator": "SEC",
        "force_live_verification": True,
    }


def test_refresh_license_profile_route_requires_existing_ria(monkeypatch) -> None:
    async def _mock_refresh(self, user_id, **_kwargs):
        assert user_id == _TEST_UID
        raise RIAIAMPolicyError(
            "Complete RIA onboarding before refreshing license data.",
            status_code=409,
        )

    monkeypatch.setattr(RIAIAMService, "refresh_ria_profile_from_license", _mock_refresh)

    client = TestClient(_authed_app(), raise_server_exceptions=False)
    response = client.post(
        "/api/ria/profile/refresh-license",
        json={"license_number": "7413463"},
    )

    assert response.status_code == 409
    payload = response.json()
    assert payload["code"] == "RIA_ONBOARDING_REQUIRED"
    assert payload["route"] == "/ria/onboarding"


# ===================================================================
# Route: POST /api/ria/onboarding/submit  (v2 extension)
# ===================================================================


def test_submit_v2_with_services_and_contact(monkeypatch) -> None:
    """Full v2 payload passes all new fields through to the service."""
    captured: dict[str, Any] = {}

    async def _mock_submit(self, user_id, **kwargs):
        captured.update(kwargs)
        captured["user_id"] = user_id
        return {"status": "submitted", "user_id": user_id}

    monkeypatch.setattr(RIAIAMService, "submit_ria_onboarding", _mock_submit)

    v2_payload = {
        "display_name": "Jane Advisor",
        "license_number": "7413463",
        "regulator": "SEC",
        "onboarding_type": "individual",
        "services_offered": ["financial_planning", "portfolio_management"],
        "fee_structure": ["flat_fee", "percentage_aum"],
        "min_engagement_amount": 10000.0,
        "min_engagement_currency": "USD",
        "certifications": ["CFP", "CFA"],
        "contact_email": "jane@advisory.com",
        "contact_phone": "+16505550101",
        "business_city": "San Francisco",
        "business_area": "Financial District",
        "business_address": "123 Market St",
        "business_pin_zip": "94105",
        "business_latitude": 37.7749,
        "business_longitude": -122.4194,
    }

    client = TestClient(_authed_app())
    response = client.post("/api/ria/onboarding/submit", json=v2_payload)

    assert response.status_code == 200
    assert captured["user_id"] == _TEST_UID
    assert captured["display_name"] == "Jane Advisor"
    assert captured["license_number"] == "7413463"
    assert captured["regulator"] == "SEC"
    assert captured["onboarding_type"] == "individual"
    assert captured["services_offered"] == ["financial_planning", "portfolio_management"]
    assert captured["fee_structure"] == ["flat_fee", "percentage_aum"]
    assert captured["min_engagement_amount"] == 10000.0
    assert captured["min_engagement_currency"] == "USD"
    assert captured["certifications"] == ["CFP", "CFA"]
    assert captured["contact_email"] == "jane@advisory.com"
    assert captured["contact_phone"] == "+16505550101"
    assert captured["business_city"] == "San Francisco"
    assert captured["business_area"] == "Financial District"
    assert captured["business_address"] == "123 Market St"
    assert captured["business_pin_zip"] == "94105"
    assert captured["business_latitude"] == 37.7749
    assert captured["business_longitude"] == -122.4194


def test_submit_v2_backward_compatible(monkeypatch) -> None:
    """Old v1 payload (no new v2 fields) still works; v2 fields get defaults."""
    captured: dict[str, Any] = {}

    async def _mock_submit(self, user_id, **kwargs):
        captured.update(kwargs)
        return {"status": "submitted", "user_id": user_id}

    monkeypatch.setattr(RIAIAMService, "submit_ria_onboarding", _mock_submit)

    v1_payload = {
        "display_name": "Legacy Advisor",
        "requested_capabilities": ["advisory"],
        "individual_legal_name": "Legacy Advisor LLC",
        "individual_crd": "1234567",
    }

    client = TestClient(_authed_app())
    response = client.post("/api/ria/onboarding/submit", json=v1_payload)

    assert response.status_code == 200
    # v2 fields should be passed with their default values
    assert captured["license_number"] is None
    assert captured["regulator"] is None
    assert captured["onboarding_type"] == "individual"
    assert captured["services_offered"] == []
    assert captured["fee_structure"] == []
    assert captured["min_engagement_amount"] is None
    assert captured["min_engagement_currency"] == "USD"
    assert captured["certifications"] == []
    assert captured["contact_email"] is None
    assert captured["contact_phone"] is None
    assert captured["business_city"] is None
    assert captured["business_area"] is None
    assert captured["business_address"] is None
    assert captured["business_pin_zip"] is None
    assert captured["business_latitude"] is None
    assert captured["business_longitude"] is None


# ===================================================================
# Service: CrdScrapeProxyService.broker_intelligence()
# ===================================================================


def test_broker_intelligence_proxy_calls_correct_url() -> None:
    """Verify the proxy sends POST /v1/ria/broker-intelligence with the right payload."""
    seen: dict[str, object] = {}

    async def handler(request: httpx.Request) -> httpx.Response:
        seen["method"] = request.method
        seen["path"] = request.url.path
        seen["payload"] = json.loads(request.content.decode("utf-8"))
        return httpx.Response(
            200,
            json={
                "verifiedName": "Andrew Garrett Kirkland",
                "crdNumber": "7413463",
                "status": "ACTIVE",
            },
        )

    service = CrdScrapeProxyService(
        base_url="https://ria-intelligence.example",
        transport=httpx.MockTransport(handler),
    )

    result = asyncio.run(service.broker_intelligence(query="7413463"))

    assert result.status_code == 200
    assert result.payload["verifiedName"] == "Andrew Garrett Kirkland"
    assert seen == {
        "method": "POST",
        "path": "/v1/ria/broker-intelligence",
        "payload": {"query": "7413463"},
    }


def test_broker_intelligence_empty_query_raises() -> None:
    """An empty query string must raise ValueError."""
    service = CrdScrapeProxyService(
        base_url="https://ria-intelligence.example",
        transport=httpx.MockTransport(lambda _: httpx.Response(200, json={})),
    )

    with pytest.raises(ValueError, match="non-empty"):
        asyncio.run(service.broker_intelligence(query=""))
