"""Tests for the cost-estimation model (rates x usage)."""

from __future__ import annotations

from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING

import pytest

from mgf.cloud import cost
from mgf.cloud.models import Flavor, FloatingIP, Server, Volume

if TYPE_CHECKING:
    from pathlib import Path

_NOW = datetime(2026, 6, 16, 12, 0, tzinfo=UTC)
_FLAVORS = [
    Flavor(id="f4", name="a4-ram8-disk20-perf1", vcpus=4, ram_mb=8192, disk_gb=20),
    Flavor(id="fx", name="custom-9-ram18-disk0", vcpus=9, ram_mb=18432, disk_gb=0),
]


def _server(name: str, flavor: str, hours_old: float | None) -> Server:
    created = (_NOW - timedelta(hours=hours_old)).isoformat() if hours_old is not None else None
    return Server(id=name, name=name, flavor=flavor, created_at=created)


def test_known_flavor_is_priced_exactly_with_accrued() -> None:
    report = cost.estimate(
        servers=[_server("ci", "a4-ram8-disk20-perf1", hours_old=10)],
        flavors=_FLAVORS,
        now=_NOW,
    )
    (line,) = report.lines
    assert line.exact is True
    assert line.hourly == 0.0160  # from the Infomaniak price table
    assert line.accrued == 0.160  # rate x 10h
    assert report.hourly == 0.0160
    assert report.daily == 0.0160 * 24
    assert report.monthly == 0.0160 * cost.HOURS_PER_MONTH
    assert report.accrued == 0.160
    assert report.any_estimated is False


def test_unknown_flavor_falls_back_to_per_resource_estimate() -> None:
    report = cost.estimate(
        servers=[_server("x", "custom-9-ram18-disk0", hours_old=2)],
        flavors=_FLAVORS,
        now=_NOW,
    )
    (line,) = report.lines
    assert line.exact is False
    # 9 vCPU x 0.00175 + 18 GB x 0.000875 + 0 disk = 0.01575 + 0.01575 = 0.0315
    assert round(line.hourly, 5) == 0.0315
    assert report.any_estimated is True


def test_missing_flavor_specs_yield_zero_not_crash() -> None:
    # server references a flavor the provider didn't list, and there are no specs
    report = cost.estimate(servers=[_server("y", "ghost", hours_old=1)], flavors=[], now=_NOW)
    (line,) = report.lines
    assert line.hourly == 0.0
    assert line.exact is False


def test_unknown_age_accrues_nothing_but_still_rates() -> None:
    report = cost.estimate(
        servers=[_server("z", "a4-ram8-disk20-perf1", hours_old=None)],
        flavors=_FLAVORS,
        now=_NOW,
    )
    (line,) = report.lines
    assert line.age_hours is None
    assert line.accrued == 0.0
    assert line.hourly == 0.0160
    assert report.any_age_unknown is True


def test_volumes_and_floating_ips_contribute() -> None:
    report = cost.estimate(
        servers=[],
        flavors=_FLAVORS,
        volumes=[Volume(id="v1", name="data", size_gb=100)],
        floating_ips=[FloatingIP(id="ip1", address="1.2.3.4")],
        rates=cost.Rates(per_floating_ip_hour=0.01),
        now=_NOW,
    )
    kinds = {line.kind: line for line in report.lines}
    assert kinds["volume"].hourly == 100 * 0.00004
    assert kinds["floating-ip"].hourly == 0.01


def test_free_floating_ips_are_omitted() -> None:
    report = cost.estimate(
        servers=[],
        flavors=[],
        floating_ips=[FloatingIP(id="ip1", address="1.2.3.4")],  # default rate 0.0
        now=_NOW,
    )
    assert all(line.kind != "floating-ip" for line in report.lines)


def test_load_rates_merges_table_and_overrides_scalars() -> None:
    rates = cost.load_rates(
        {
            "currency": "USD",
            "per_vcpu_hour": 0.01,
            "flavor_hourly": {"a4-ram8-disk20-perf1": 0.0200, "new-flavor": 0.5},
        }
    )
    assert rates.currency == "USD"
    assert rates.per_vcpu_hour == 0.01
    # override applied…
    assert rates.flavor_hourly["a4-ram8-disk20-perf1"] == 0.0200
    # …new entry added…
    assert rates.flavor_hourly["new-flavor"] == 0.5
    # …and a built-in not in the override survives the merge.
    assert rates.flavor_hourly["a1-ram2-disk0"] == 0.0050


def test_load_rates_no_overrides_is_defaults() -> None:
    assert cost.load_rates() == cost.Rates()
    assert cost.load_rates(None) == cost.Rates()


def test_bad_created_at_is_treated_as_unknown_age() -> None:
    report = cost.estimate(
        servers=[Server(id="s", name="s", flavor="a4-ram8-disk20-perf1", created_at="not-a-date")],
        flavors=_FLAVORS,
        now=_NOW,
    )
    assert report.lines[0].age_hours is None


def test_naive_created_at_is_assumed_utc() -> None:
    report = cost.estimate(
        servers=[
            Server(
                id="s", name="s", flavor="a4-ram8-disk20-perf1", created_at="2026-06-16T10:00:00"
            )
        ],
        flavors=_FLAVORS,
        now=_NOW,
    )
    assert report.lines[0].age_hours == 2.0


def test_rates_from_config_explicit_path(tmp_path: Path) -> None:
    p = tmp_path / "rates.toml"
    p.write_text(
        'currency = "USD"\nper_vcpu_hour = 0.01\n[flavor_hourly]\n"a4-ram8-disk20-perf1" = 0.02\n',
        encoding="utf-8",
    )
    rates = cost.rates_from_config(p)
    assert rates.currency == "USD"
    assert rates.per_vcpu_hour == 0.01
    assert rates.flavor_hourly["a4-ram8-disk20-perf1"] == 0.02  # overridden
    assert rates.flavor_hourly["a1-ram2-disk0"] == 0.0050  # built-in still present


def test_rates_from_config_accepts_rates_section_wrapper(tmp_path: Path) -> None:
    p = tmp_path / "cfg.toml"
    p.write_text("[rates]\nper_gb_volume_hour = 0.0001\n", encoding="utf-8")
    assert cost.rates_from_config(p).per_gb_volume_hour == 0.0001


def test_rates_from_config_reads_env_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    p = tmp_path / "env.toml"
    p.write_text('currency = "GBP"\n', encoding="utf-8")
    monkeypatch.setenv(cost._RATES_ENV, str(p))
    assert cost.rates_from_config().currency == "GBP"


def test_rates_from_config_missing_explicit_path_raises(tmp_path: Path) -> None:
    # EH-01: a typed ConfigError (not a raw builtin) so callers can branch on it.
    from mgf.cloud.exceptions import ConfigError

    with pytest.raises(ConfigError):
        cost.rates_from_config(tmp_path / "absent.toml")


def test_rates_from_config_falls_back_to_defaults(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.delenv(cost._RATES_ENV, raising=False)
    monkeypatch.setattr(cost, "_default_rates_paths", lambda: (tmp_path / "none.toml",))
    assert cost.rates_from_config() == cost.Rates()
