"""Tests for app/integrations/pricing/profiles.py.

Acceptance criteria covered
----------------------------
1. ``load_profile("manual")`` succeeds and returns a valid ``ManualProfile``.
2. ``load_profile("tibber")`` succeeds and returns a valid ``TibberProfile``.
3. Missing profile file raises ``ProfileNotFoundError``.
4. Malformed YAML (missing required fields) raises ``ProfileValidationError``.
5. Wrong kind in YAML raises ``ProfileValidationError``.
6. ``validate_values`` accepts conforming values for both kinds.
7. ``validate_values`` fills in default values (``ode``, ``sell_adjust``,
   tibber ``management_fee``) when absent.
8. ``validate_values`` raises ``ProfileValidationError`` for missing required
   fields (no default).
9. ``validate_values`` raises ``ProfileValidationError`` for wrong-typed fields.
10. ``list_profiles()`` returns both profiles in a list of dicts.
"""

from __future__ import annotations

import textwrap
from pathlib import Path
from unittest.mock import patch

import pytest
import yaml

from app.integrations.pricing.profiles import (
    ManualProfile,
    ProfileNotFoundError,
    ProfileValidationError,
    TibberProfile,
    list_profiles,
    load_profile,
    validate_values,
)


# ---------------------------------------------------------------------------
# 1-2: load_profile happy path
# ---------------------------------------------------------------------------


class TestLoadProfileManual:
    """Validate that the shipped manual.yaml loads and validates correctly."""

    def test_returns_manual_profile_instance(self) -> None:
        profile = load_profile("manual")
        assert isinstance(profile, ManualProfile)

    def test_kind_is_manual(self) -> None:
        profile = load_profile("manual")
        assert profile.kind == "manual"

    def test_has_label(self) -> None:
        profile = load_profile("manual")
        assert isinstance(profile.label, str) and profile.label

    def test_dual_tariff_is_true(self) -> None:
        profile = load_profile("manual")
        assert profile.energy.dual_tariff is True

    def test_energy_buy_has_normal_and_dal(self) -> None:
        profile = load_profile("manual")
        assert profile.energy.buy.normal.unit == "EUR/kWh"
        assert profile.energy.buy.dal.unit == "EUR/kWh"

    def test_energy_sell_has_normal_and_dal(self) -> None:
        profile = load_profile("manual")
        assert profile.energy.sell.normal.unit == "EUR/kWh"
        assert profile.energy.sell.dal.unit == "EUR/kWh"

    def test_energy_tax_unit(self) -> None:
        profile = load_profile("manual")
        assert profile.energy.energy_tax.unit == "EUR/kWh"
        assert profile.energy.energy_tax.default is None  # required field, no default

    def test_ode_has_default_zero(self) -> None:
        profile = load_profile("manual")
        assert profile.energy.ode.default == 0

    def test_standing_fields(self) -> None:
        profile = load_profile("manual")
        assert profile.standing.network_fee.unit == "EUR/month"
        assert profile.standing.management_fee.unit == "EUR/month"

    def test_credits_heffingskorting(self) -> None:
        profile = load_profile("manual")
        assert profile.credits.heffingskorting.unit == "EUR/year"

    def test_no_concrete_values_in_profile(self) -> None:
        """Profile YAML must not contain any concrete price numbers."""
        profile_path = (
            Path(__file__).parent.parent
            / "app/integrations/pricing/profiles/manual.yaml"
        )
        with profile_path.open() as fh:
            raw = yaml.safe_load(fh)
        # Leaf nodes should only have 'unit' and optionally 'default: 0',
        # not actual price values like 0.133 or 0.127.
        energy = raw.get("energy", {})
        buy = energy.get("buy", {})
        # Leaf buy nodes: only 'unit' key, no numeric value key.
        assert set(buy["normal"].keys()) == {"unit"}, (
            "buy.normal leaf must only have 'unit'"
        )
        assert set(buy["dal"].keys()) == {"unit"}, (
            "buy.dal leaf must only have 'unit'"
        )


class TestLoadProfileTibber:
    """Validate that the shipped tibber.yaml loads and validates correctly."""

    def test_returns_tibber_profile_instance(self) -> None:
        profile = load_profile("tibber")
        assert isinstance(profile, TibberProfile)

    def test_kind_is_tibber(self) -> None:
        profile = load_profile("tibber")
        assert profile.kind == "tibber"

    def test_has_label(self) -> None:
        profile = load_profile("tibber")
        assert isinstance(profile.label, str) and profile.label

    def test_energy_source_is_tibber_api(self) -> None:
        profile = load_profile("tibber")
        assert profile.energy.source == "tibber_api"

    def test_energy_tax_unit(self) -> None:
        profile = load_profile("tibber")
        assert profile.energy.energy_tax.unit == "EUR/kWh"
        assert profile.energy.energy_tax.default is None  # required

    def test_sell_adjust_has_default_zero(self) -> None:
        profile = load_profile("tibber")
        assert profile.energy.sell_adjust.default == 0

    def test_management_fee_has_default(self) -> None:
        profile = load_profile("tibber")
        assert profile.standing.management_fee.default is not None
        assert isinstance(profile.standing.management_fee.default, float)

    def test_network_fee_unit(self) -> None:
        profile = load_profile("tibber")
        assert profile.standing.network_fee.unit == "EUR/month"

    def test_credits_heffingskorting(self) -> None:
        profile = load_profile("tibber")
        assert profile.credits.heffingskorting.unit == "EUR/year"


# ---------------------------------------------------------------------------
# 3-5: load_profile error cases
# ---------------------------------------------------------------------------


class TestLoadProfileErrors:
    def test_missing_profile_raises_not_found(self) -> None:
        with pytest.raises(ProfileNotFoundError, match="nonexistent"):
            load_profile("nonexistent")

    def test_missing_required_fields_raises_validation_error(
        self, tmp_path: Path
    ) -> None:
        """A YAML missing required fields raises ProfileValidationError."""
        bad_yaml = textwrap.dedent(
            """\
            kind: manual
            label: Bad manual profile
            # missing energy / standing / credits sections entirely
            """
        )
        bad_path = tmp_path / "manual.yaml"
        bad_path.write_text(bad_yaml)

        with patch("app.integrations.pricing.profiles._PROFILES_DIR", tmp_path):
            with pytest.raises(ProfileValidationError, match="manual"):
                load_profile("manual")

    def test_wrong_type_raises_validation_error(self, tmp_path: Path) -> None:
        """A YAML with a wrong type (string where dict is expected) raises ProfileValidationError."""
        bad_yaml = textwrap.dedent(
            """\
            kind: tibber
            label: Wrong type profile
            energy: "should be a mapping not a string"
            standing:
              management_fee: { unit: EUR/month, default: 5.99 }
              network_fee: { unit: EUR/month }
            credits:
              heffingskorting: { unit: EUR/year }
            """
        )
        bad_path = tmp_path / "tibber.yaml"
        bad_path.write_text(bad_yaml)

        with patch("app.integrations.pricing.profiles._PROFILES_DIR", tmp_path):
            with pytest.raises(ProfileValidationError, match="tibber"):
                load_profile("tibber")

    def test_unknown_kind_raises_validation_error(self, tmp_path: Path) -> None:
        """A YAML with an unknown kind raises ProfileValidationError."""
        bad_yaml = textwrap.dedent(
            """\
            kind: unknown_kind
            label: Unknown kind profile
            """
        )
        bad_path = tmp_path / "unknown_kind.yaml"
        bad_path.write_text(bad_yaml)

        with patch("app.integrations.pricing.profiles._PROFILES_DIR", tmp_path):
            with pytest.raises(ProfileValidationError, match="unknown_kind"):
                load_profile("unknown_kind")


# ---------------------------------------------------------------------------
# 6-9: validate_values
# ---------------------------------------------------------------------------

# A complete, conforming set of manual contract values.
_VALID_MANUAL_VALUES = {
    "energy": {
        "buy": {"normal": 0.133, "dal": 0.127},
        "sell": {"normal": 0.05, "dal": 0.05},
        "energy_tax": 0.1108,
        "ode": 0.0,
    },
    "standing": {
        "network_fee": 9.87,
        "management_fee": 9.87,
    },
    "credits": {
        "heffingskorting": 600.0,
    },
}

# A complete, conforming set of tibber contract values.
_VALID_TIBBER_VALUES = {
    "energy": {
        "energy_tax": 0.1108,
        "sell_adjust": 0.0,
    },
    "standing": {
        "management_fee": 5.99,
        "network_fee": 9.87,
    },
    "credits": {
        "heffingskorting": 600.0,
    },
}


class TestValidateValuesManual:
    def test_valid_values_accepted(self) -> None:
        filled = validate_values("manual", dict(_VALID_MANUAL_VALUES))
        # Should not raise and should return a dict.
        assert isinstance(filled, dict)

    def test_ode_default_applied_when_absent(self) -> None:
        """When 'ode' is not in values, the default (0) should be inserted."""
        values = {
            "energy": {
                "buy": {"normal": 0.133, "dal": 0.127},
                "sell": {"normal": 0.05, "dal": 0.05},
                "energy_tax": 0.1108,
                # ode is absent
            },
            "standing": {
                "network_fee": 9.87,
                "management_fee": 9.87,
            },
            "credits": {"heffingskorting": 600.0},
        }
        filled = validate_values("manual", values)
        assert filled["energy"]["ode"] == 0

    def test_missing_energy_tax_raises(self) -> None:
        values = {
            "energy": {
                "buy": {"normal": 0.133, "dal": 0.127},
                "sell": {"normal": 0.05, "dal": 0.05},
                # energy_tax absent — no default
            },
            "standing": {"network_fee": 9.87, "management_fee": 9.87},
            "credits": {"heffingskorting": 600.0},
        }
        with pytest.raises(ProfileValidationError, match="energy_tax"):
            validate_values("manual", values)

    def test_missing_buy_normal_raises(self) -> None:
        values = {
            "energy": {
                "buy": {"dal": 0.127},  # normal absent
                "sell": {"normal": 0.05, "dal": 0.05},
                "energy_tax": 0.1108,
                "ode": 0.0,
            },
            "standing": {"network_fee": 9.87, "management_fee": 9.87},
            "credits": {"heffingskorting": 600.0},
        }
        with pytest.raises(ProfileValidationError, match="normal"):
            validate_values("manual", values)

    def test_wrong_type_for_energy_tax_raises(self) -> None:
        values = {
            "energy": {
                "buy": {"normal": 0.133, "dal": 0.127},
                "sell": {"normal": 0.05, "dal": 0.05},
                "energy_tax": "not_a_number",  # wrong type
                "ode": 0.0,
            },
            "standing": {"network_fee": 9.87, "management_fee": 9.87},
            "credits": {"heffingskorting": 600.0},
        }
        with pytest.raises(ProfileValidationError, match="energy_tax"):
            validate_values("manual", values)

    def test_missing_heffingskorting_raises(self) -> None:
        values = {
            "energy": {
                "buy": {"normal": 0.133, "dal": 0.127},
                "sell": {"normal": 0.05, "dal": 0.05},
                "energy_tax": 0.1108,
                "ode": 0.0,
            },
            "standing": {"network_fee": 9.87, "management_fee": 9.87},
            "credits": {},  # heffingskorting absent — no default
        }
        with pytest.raises(ProfileValidationError, match="heffingskorting"):
            validate_values("manual", values)


class TestValidateValuesTibber:
    def test_valid_values_accepted(self) -> None:
        filled = validate_values("tibber", dict(_VALID_TIBBER_VALUES))
        assert isinstance(filled, dict)

    def test_sell_adjust_default_applied_when_absent(self) -> None:
        values = {
            "energy": {
                "energy_tax": 0.1108,
                # sell_adjust absent — has default 0
            },
            "standing": {"management_fee": 5.99, "network_fee": 9.87},
            "credits": {"heffingskorting": 600.0},
        }
        filled = validate_values("tibber", values)
        assert filled["energy"]["sell_adjust"] == 0

    def test_management_fee_default_applied_when_absent(self) -> None:
        values = {
            "energy": {"energy_tax": 0.1108, "sell_adjust": 0.0},
            "standing": {
                "network_fee": 9.87,
                # management_fee absent — has a default
            },
            "credits": {"heffingskorting": 600.0},
        }
        filled = validate_values("tibber", values)
        assert "management_fee" in filled["standing"]
        assert isinstance(filled["standing"]["management_fee"], float)

    def test_missing_energy_tax_raises(self) -> None:
        values = {
            "energy": {"sell_adjust": 0.0},  # energy_tax absent — no default
            "standing": {"management_fee": 5.99, "network_fee": 9.87},
            "credits": {"heffingskorting": 600.0},
        }
        with pytest.raises(ProfileValidationError, match="energy_tax"):
            validate_values("tibber", values)

    def test_missing_network_fee_raises(self) -> None:
        values = {
            "energy": {"energy_tax": 0.1108, "sell_adjust": 0.0},
            "standing": {"management_fee": 5.99},  # network_fee absent — no default
            "credits": {"heffingskorting": 600.0},
        }
        with pytest.raises(ProfileValidationError, match="network_fee"):
            validate_values("tibber", values)

    def test_wrong_type_for_sell_adjust_raises(self) -> None:
        values = {
            "energy": {"energy_tax": 0.1108, "sell_adjust": "zero"},  # wrong type
            "standing": {"management_fee": 5.99, "network_fee": 9.87},
            "credits": {"heffingskorting": 600.0},
        }
        with pytest.raises(ProfileValidationError, match="sell_adjust"):
            validate_values("tibber", values)


# ---------------------------------------------------------------------------
# 10: list_profiles
# ---------------------------------------------------------------------------


class TestListProfiles:
    def test_returns_list_of_dicts(self) -> None:
        profiles = list_profiles()
        assert isinstance(profiles, list)
        for item in profiles:
            assert isinstance(item, dict)

    def test_contains_manual_and_tibber(self) -> None:
        profiles = list_profiles()
        kinds = {p["kind"] for p in profiles}
        assert "manual" in kinds
        assert "tibber" in kinds

    def test_each_entry_has_label(self) -> None:
        profiles = list_profiles()
        for p in profiles:
            assert "label" in p and isinstance(p["label"], str)
