"""Tests for core data models — Exposure, Portfolio, and config loader."""

import pytest
from pydantic import ValidationError

from creditriskengine.core.config import load_jurisdiction_config
from creditriskengine.core.exposure import Collateral, Exposure
from creditriskengine.core.portfolio import Portfolio
from creditriskengine.core.types import (
    CollateralType,
    CreditRiskApproach,
    Jurisdiction,
    SAExposureClass,
)


class TestExposure:
    def test_minimal_creation(self) -> None:
        e = Exposure(
            exposure_id="E001",
            counterparty_id="C001",
            ead=1000.0,
            drawn_amount=800.0,
            jurisdiction=Jurisdiction.BCBS,
            approach=CreditRiskApproach.SA,
        )
        assert e.ead == 1000.0
        assert e.is_defaulted is False

    def test_pd_field_allows_valid_range(self) -> None:
        e = Exposure(
            exposure_id="E001",
            counterparty_id="C001",
            ead=100.0,
            drawn_amount=100.0,
            jurisdiction=Jurisdiction.EU,
            approach=CreditRiskApproach.FIRB,
            pd=0.02,
        )
        assert e.pd == 0.02

    def test_pd_rejects_above_one(self) -> None:
        with pytest.raises((ValueError, ValidationError)):
            Exposure(
                exposure_id="E001",
                counterparty_id="C001",
                ead=100.0,
                drawn_amount=100.0,
                jurisdiction=Jurisdiction.EU,
                approach=CreditRiskApproach.FIRB,
                pd=1.5,
            )

    def test_pd_rejects_negative(self) -> None:
        with pytest.raises((ValueError, ValidationError)):
            Exposure(
                exposure_id="E001",
                counterparty_id="C001",
                ead=100.0,
                drawn_amount=100.0,
                jurisdiction=Jurisdiction.EU,
                approach=CreditRiskApproach.FIRB,
                pd=-0.01,
            )

    def test_ead_non_negative(self) -> None:
        with pytest.raises((ValueError, ValidationError)):
            Exposure(
                exposure_id="E001",
                counterparty_id="C001",
                ead=-100.0,
                drawn_amount=100.0,
                jurisdiction=Jurisdiction.BCBS,
                approach=CreditRiskApproach.SA,
            )


class TestCollateral:
    def test_basic(self) -> None:
        c = Collateral(collateral_type=CollateralType.CASH, value=100_000.0)
        assert c.collateral_type == CollateralType.CASH
        assert c.haircut is None


class TestPortfolio:
    def _make_exposure(self, eid: str, ead: float, defaulted: bool = False) -> Exposure:
        return Exposure(
            exposure_id=eid,
            counterparty_id=f"C_{eid}",
            ead=ead,
            drawn_amount=ead,
            jurisdiction=Jurisdiction.BCBS,
            approach=CreditRiskApproach.SA,
            sa_exposure_class=SAExposureClass.CORPORATE,
            is_defaulted=defaulted,
        )

    def test_add_and_len(self) -> None:
        p = Portfolio()
        p.add_exposure(self._make_exposure("E1", 100.0))
        p.add_exposure(self._make_exposure("E2", 200.0))
        assert len(p) == 2

    def test_total_ead(self) -> None:
        p = Portfolio()
        p.add_exposure(self._make_exposure("E1", 100.0))
        p.add_exposure(self._make_exposure("E2", 200.0))
        assert p.total_ead() == pytest.approx(300.0)

    def test_iteration(self) -> None:
        p = Portfolio()
        p.add_exposure(self._make_exposure("E1", 100.0))
        p.add_exposure(self._make_exposure("E2", 200.0))
        eids = [e.exposure_id for e in p]
        assert eids == ["E1", "E2"]

    def test_filter_defaulted(self) -> None:
        p = Portfolio()
        p.add_exposure(self._make_exposure("E1", 100.0, defaulted=False))
        p.add_exposure(self._make_exposure("E2", 200.0, defaulted=True))
        assert len(p.filter_defaulted()) == 1
        assert len(p.filter_non_defaulted()) == 1

    def test_filter_by_approach(self) -> None:
        p = Portfolio()
        p.add_exposure(self._make_exposure("E1", 100.0))
        result = p.filter_by_approach(CreditRiskApproach.SA)
        assert len(result) == 1


class TestExposurePDValidation:
    """Cover lines 115-116: PD validator raises ValueError for out-of-range PD."""

    def test_pd_out_of_range_message(self) -> None:
        """Trigger the explicit ValueError message branch (lines 115-116)."""
        from creditriskengine.core.exposure import Exposure

        # Call the validator method directly to bypass Pydantic field constraints
        with pytest.raises(ValueError, match="PD must be between 0 and 1"):
            Exposure.pd_floor_check(2.0)

    def test_pd_negative_out_of_range(self) -> None:
        with pytest.raises(ValueError, match="PD must be between 0 and 1"):
            Exposure.pd_floor_check(-0.5)

    def test_pd_validator_directly_above_one(self) -> None:
        """Ensure lines 115-116 are reached by calling validator directly."""
        with pytest.raises(ValueError, match="PD must be between 0 and 1, got 1.1"):
            Exposure.pd_floor_check(1.1)

    def test_pd_validator_directly_below_zero(self) -> None:
        """Ensure lines 115-116 are reached by calling validator directly."""
        with pytest.raises(ValueError, match="PD must be between 0 and 1, got -0.01"):
            Exposure.pd_floor_check(-0.01)


class TestCoreConfigDelegation:
    """Verify core.config delegates to regulatory.loader correctly."""

    def test_load_jurisdiction_config_returns_dict(self) -> None:
        config = load_jurisdiction_config(Jurisdiction.EU)
        assert isinstance(config, dict)
        assert "regulator" in config or "jurisdiction" in config

    def test_load_jurisdiction_config_matches_loader(self) -> None:
        from creditriskengine.regulatory.loader import load_config

        config_via_core = load_jurisdiction_config(Jurisdiction.UK)
        config_via_loader = load_config(Jurisdiction.UK)
        assert config_via_core == config_via_loader
