"""Targeted tests for remaining coverage gaps — v2.

Covers uncovered lines in:
- credit/filing_validate.py (lines 68-73, 110, 129-132, 151-154)
- experiences/openapi.py (lines 14-15)
- experiences/rest_api.py (lines 39, 45-46, 54-55, 228-229, 245-246)
- orchestration/run.py (lines 137, 141-142, 149, 157-158, 174-175)
"""

from __future__ import annotations

import csv
import json
import sys
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

starlette = pytest.importorskip("starlette", reason="starlette not installed")
from starlette.testclient import TestClient  # noqa: E402

from orxaq.credit.filing_validate import validate_filing_package  # noqa: E402
from orxaq.experiences.openapi import generate_spec  # noqa: E402
from orxaq.experiences.rest_api import make_app  # noqa: E402
from orxaq.orchestration.run import (  # noqa: E402
    _read_ecl_total,
    _read_stress_summaries,
    _run_fair_lending,
)

# ---------------------------------------------------------------------------
# credit/filing_validate.py
# ---------------------------------------------------------------------------


class TestFilingValidateManifestBadJSON:
    """Cover lines 68-73: manifest.json is not valid JSON."""

    def test_manifest_parse_error(self):
        with tempfile.TemporaryDirectory() as td:
            out = Path(td)
            # Write a bad manifest
            (out / "manifest.json").write_text("{bad json", encoding="utf-8")
            result = validate_filing_package(td)
        # manifest_parseable check should be False
        names = {c["name"]: c for c in result["checks"]}
        assert not names["manifest_parseable"]["passed"]
        assert "JSON error" in names["manifest_parseable"]["detail"]


class TestFilingValidateEmptyManifestFiles:
    """Cover line 110: manifest exists but has no 'files' key → sha256 check fails."""

    def test_no_files_in_manifest(self):
        with tempfile.TemporaryDirectory() as td:
            out = Path(td)
            (out / "manifest.json").write_text(
                json.dumps({"generated_at": "2026-02-18T00:00:00Z"}),
                encoding="utf-8",
            )
            result = validate_filing_package(td)
        names = {c["name"]: c for c in result["checks"]}
        assert not names["sha256_checksums"]["passed"]
        assert "No file entries" in names["sha256_checksums"]["detail"]


class TestFilingValidateCECLBadJSON:
    """Cover lines 129-132: cecl_disclosure.json is not valid JSON."""

    def test_cecl_json_error(self):
        with tempfile.TemporaryDirectory() as td:
            out = Path(td)
            (out / "cecl_disclosure.json").write_text("{bad cecl", encoding="utf-8")
            result = validate_filing_package(td)
        names = {c["name"]: c for c in result["checks"]}
        assert not names["cecl_schema"]["passed"]
        assert "JSON error" in names["cecl_schema"]["detail"]


class TestFilingValidateCCARBadJSON:
    """Cover lines 151-154: ccar_export.json is not valid JSON."""

    def test_ccar_json_error(self):
        with tempfile.TemporaryDirectory() as td:
            out = Path(td)
            (out / "ccar_export.json").write_text("{bad ccar", encoding="utf-8")
            result = validate_filing_package(td)
        names = {c["name"]: c for c in result["checks"]}
        assert not names["ccar_schema"]["passed"]
        assert "JSON error" in names["ccar_schema"]["detail"]


# ---------------------------------------------------------------------------
# experiences/openapi.py
# ---------------------------------------------------------------------------


class TestOpenAPISpecImportError:
    """Cover lines 14-15: exception handler when orxaq import raises."""

    def test_fallback_version_on_import_error(self):
        with patch.dict(sys.modules, {"orxaq": None}):
            spec = generate_spec()
        # Should fall back to "1.0.0"
        assert spec["info"]["version"] == "1.0.0"


# ---------------------------------------------------------------------------
# experiences/rest_api.py
# ---------------------------------------------------------------------------


@pytest.fixture(scope="module")
def open_client():
    return TestClient(make_app())


class TestRestAPIAuthGateRejects:
    """Cover line 39: _AuthGate returns 401 when not authenticated."""

    def test_401_on_no_api_key(self):
        from orxaq.enterprise.auth import APIKeyStore, AuthMiddleware

        store = APIKeyStore()
        store.generate_key("key1")
        mw = AuthMiddleware(key_store=store)
        client = TestClient(make_app(auth_middleware=mw))
        # No X-API-Key header → 401
        resp = client.post(
            "/api/v1/ecl/compute",
            json={"portfolio_size": 10},
        )
        assert resp.status_code == 401


class TestRestAPIAuthGatePasses:
    """Cover line 39: _AuthGate calls next when authenticated."""

    def test_200_with_valid_api_key(self):
        from orxaq.enterprise.auth import APIKeyStore, AuthMiddleware

        store = APIKeyStore()
        raw_key, _ = store.generate_key("key1")
        mw = AuthMiddleware(key_store=store)
        client = TestClient(make_app(auth_middleware=mw))
        resp = client.get(
            "/api/v1/health",  # in _NO_AUTH, but also test with authenticated endpoint
        )
        assert resp.status_code == 200
        # Authenticated request to a protected endpoint
        resp2 = client.get(
            "/api/v1/ontology",
            headers={"X-API-Key": raw_key},
        )
        assert resp2.status_code == 200


class TestRestAPIParseBodyException:
    """Cover lines 45-46: _parse_body catches invalid JSON body."""

    def test_invalid_body_falls_back_to_empty(self, open_client):
        # Send non-JSON body with JSON content-type → parse error falls back to {}
        # When body is {} the defaults kick in (portfolio_size=200 > 0) → 200
        resp = open_client.post(
            "/api/v1/ecl/compute",
            content=b"not-json",
            headers={"Content-Type": "application/json"},
        )
        # Either 200 (uses defaults) or 400, but not 5xx
        assert resp.status_code < 500


class TestRestAPIHealthImportError:
    """Cover lines 54-55: _health fallback when orxaq import fails."""

    def test_health_version_fallback(self, open_client):
        with patch.dict(sys.modules, {"orxaq": None}):
            resp = open_client.get("/api/v1/health")
        assert resp.status_code == 200
        # orxaq not importable → version="0.0.0"
        assert resp.json()["version"] == "0.0.0"


class TestRestAPIRegulatoryJSONParseError:
    """Cover lines 228-229: raw export is not JSON-parseable → returns raw."""

    def test_non_json_export_passthrough(self, open_client):
        # Patch the exporter where it's imported locally in the handler
        with patch("orxaq.credit.regulatory_export.RegulatoryExporter") as mock_exporter_cls:
            mock_inst = MagicMock()
            mock_inst.export_cecl.return_value = "<<not-json>>"
            mock_exporter_cls.return_value = mock_inst
            resp = open_client.post(
                "/api/v1/regulatory/export",
                json={"framework": "cecl", "format": "json"},
            )
        assert resp.status_code == 200
        data = resp.json()
        assert data["content"] == "<<not-json>>"


class TestRestAPIOpenAPIDocs:
    """Cover lines 245-246: /docs returns HTML."""

    def test_docs_endpoint_html(self, open_client):
        resp = open_client.get("/docs")
        assert resp.status_code == 200
        assert "text/html" in resp.headers["content-type"]
        assert "openapi" in resp.text.lower()


# ---------------------------------------------------------------------------
# orchestration/run.py
# ---------------------------------------------------------------------------


class TestReadECLTotalMissing:
    """Cover line 137: _read_ecl_total when file does not exist."""

    def test_returns_zero_when_missing(self):
        with tempfile.TemporaryDirectory() as td:
            result = _read_ecl_total(Path(td))
        assert result == 0.0


class TestReadECLTotalBadJSON:
    """Cover lines 141-142: _read_ecl_total catches JSON / value errors."""

    def test_bad_json_returns_zero(self):
        with tempfile.TemporaryDirectory() as td:
            p = Path(td) / "cecl_disclosure.json"
            p.write_text("{bad", encoding="utf-8")
            result = _read_ecl_total(Path(td))
        assert result == 0.0

    def test_non_numeric_ecl_returns_zero(self):
        with tempfile.TemporaryDirectory() as td:
            p = Path(td) / "cecl_disclosure.json"
            p.write_text(
                json.dumps({"quantitative_results": {"ecl_estimate": "not-a-number"}}),
                encoding="utf-8",
            )
            result = _read_ecl_total(Path(td))
        assert result == 0.0


class TestReadStressSummariesMissing:
    """Cover line 149: _read_stress_summaries when file does not exist."""

    def test_returns_empty_when_missing(self):
        with tempfile.TemporaryDirectory() as td:
            result = _read_stress_summaries(Path(td))
        assert result == {}


class TestReadStressSummariesBadCSV:
    """Cover lines 157-158: _read_stress_summaries catches OSError/csv.Error."""

    def test_bad_csv_content_returns_empty(self):
        with tempfile.TemporaryDirectory() as td:
            p = Path(td) / "scenario_comparison.csv"
            p.write_bytes(b"\xff\xfe")  # invalid UTF-8
            with patch("orxaq.orchestration.run.csv.DictReader") as mock_reader:
                mock_reader.side_effect = csv.Error("malformed")
                result = _read_stress_summaries(Path(td))
        assert result == {}


class TestRunFairLendingException:
    """Cover lines 174-175: _run_fair_lending swallows exceptions."""

    def test_exception_returns_empty_list(self):
        # FairLendingAnalyzer is imported locally, patch at source module
        with patch("orxaq.credit.fair_lending.FairLendingAnalyzer") as mock_analyzer_cls:
            mock_inst = MagicMock()
            mock_inst.decompose_effect.side_effect = RuntimeError("test error")
            mock_analyzer_cls.return_value = mock_inst
            result = _run_fair_lending()
        assert result == []
