"""Unit tests for the Infomaniak Manager-API subsystem (``mgf.cloud.manager``)
plus its CLI verb. No network: the real adapter is driven through an injected
fake :class:`~mgf.cloud.manager.infomaniak.Transport` (TS-02)."""

from __future__ import annotations

import argparse
import json
import urllib.error
from typing import Any

import pytest

from mgf.cloud.cli._manager import run_manager
from mgf.cloud.exceptions import (
    CredentialsError,
    ManagerApiError,
    ProviderNotFoundError,
    ResourceNotFoundError,
)
from mgf.cloud.manager import (
    AVAILABLE_MANAGERS,
    DnsRecord,
    HostingSite,
    ManagerCredentials,
    Product,
    WebHosting,
    available_managers,
    get_manager,
    load_manager_credentials,
)
from mgf.cloud.manager import config as mgr_config
from mgf.cloud.manager.fake import FakeManager
from mgf.cloud.manager.infomaniak import HttpResponse, InfomaniakManager, UrllibTransport

# --- canned API payloads (shape mirrors the live api.infomaniak.com) ---------
PRODUCTS = [
    {"id": 743284, "service_name": "hosting", "customer_name": "magogi.org", "account_id": 1624618},
    {"id": 2047496, "service_name": "domain", "customer_name": "magogi.org", "account_id": 1624618},
]
HOSTING = {
    "web_hosting_id": 743284,
    "account_id": 1624618,
    "customer_name": "magogi.org",
    "version": 2,
    "is_free": False,
    "is_zero_price": True,
}
SITE = {
    "id": 1349857,
    "parent_id": 743284,
    "main_fqdn": "magogi.org",
    "directory": "/",
    "hosting_type": "starter",
    "ssl_status": "free",
    "can_install_applications": True,
}
DNS = [{"id": "1", "source": "", "type": "A", "ttl": 3600, "target": "128.65.195.180"}]


def _ok(data: Any) -> dict[str, Any]:
    return {"result": "success", "data": data}


def _err(code: str, desc: str) -> dict[str, Any]:
    return {"result": "error", "error": {"code": code, "description": desc}}


class FakeTransport:
    """Routes a path-suffix -> (status, json), or scripts a response sequence."""

    def __init__(
        self,
        routes: dict[str, tuple[int, dict[str, Any]]] | None = None,
        script: list[tuple[int, dict[str, Any]]] | None = None,
    ) -> None:
        self.routes = routes or {}
        self.script = list(script or [])
        self.calls: list[str] = []

    def get(self, url: str, headers: dict[str, str], timeout: float) -> HttpResponse:
        self.calls.append(url)
        assert headers["Authorization"].startswith("Bearer ")
        assert timeout > 0
        if self.script:
            status, body = self.script.pop(0)
            return HttpResponse(status=status, body=json.dumps(body).encode())
        for suffix, (status, body) in self.routes.items():
            if url.endswith(suffix):
                return HttpResponse(status=status, body=json.dumps(body).encode())
        return HttpResponse(status=404, body=json.dumps(_err("not_found", "nope")).encode())


_ROUTES = {
    "1/products": (200, _ok(PRODUCTS)),
    "1/web_hostings/743284": (200, _ok(HOSTING)),
    "1/web_hostings/743284/sites": (200, _ok([SITE])),
    "1/web_hostings/743284/sites/1349857": (200, _ok(SITE)),
    "2/zones/magogi.org/records": (200, _ok(DNS)),
}


def _live(routes: dict[str, tuple[int, dict[str, Any]]], **kw: Any) -> InfomaniakManager:
    creds = mgr_config.from_token("tok-abc")
    return InfomaniakManager(creds, transport=FakeTransport(routes), retries=0, **kw)


# --- models ------------------------------------------------------------------
def test_web_hosting_model() -> None:
    wh = WebHosting(id=743284, account_id=1, customer_name="magogi.org", is_zero_price=True)
    assert wh.id == 743284
    assert wh.is_zero_price


def test_site_tier_helpers() -> None:
    starter = HostingSite(id=1, hosting_id=2, hosting_type="starter")
    assert starter.is_starter
    assert not starter.has_ssh
    pro = HostingSite(id=1, hosting_id=2, hosting_type="hosting")
    assert not pro.is_starter
    assert pro.has_ssh
    unknown = HostingSite(id=1, hosting_id=2, hosting_type="")
    assert not unknown.has_ssh  # empty type is not asserted as ssh-capable


def test_models_are_frozen() -> None:
    rec = DnsRecord(id="1", source="www", type="A")
    with pytest.raises(Exception):  # noqa: B017 - pydantic frozen raises ValidationError
        rec.type = "CNAME"  # type: ignore[misc]


# --- config ------------------------------------------------------------------
def test_from_token_and_require_complete() -> None:
    creds = mgr_config.from_token("abc")
    assert isinstance(creds, ManagerCredentials)
    assert creds.token.get_secret_value() == "abc"


def test_empty_token_rejected() -> None:
    with pytest.raises(CredentialsError):
        mgr_config.from_token("   ")


def test_non_https_base_url_rejected() -> None:
    with pytest.raises(CredentialsError):
        ManagerCredentials(
            token=mgr_config.SecretStr("x"), base_url="http://api"
        ).require_complete()


def test_from_file_bare(tmp_path: Any) -> None:
    f = tmp_path / "t.txt"
    f.write_text("bare-token-value\n", encoding="utf-8")
    assert mgr_config.from_file(f).token.get_secret_value() == "bare-token-value"


def test_from_file_labelled(tmp_path: Any) -> None:
    f = tmp_path / "t.txt"
    f.write_text("token_name: web\ntoken: LABELLED123\nscope: all\n", encoding="utf-8")
    assert mgr_config.from_file(f).token.get_secret_value() == "LABELLED123"


def test_from_file_missing(tmp_path: Any) -> None:
    with pytest.raises(CredentialsError):
        mgr_config.from_file(tmp_path / "nope.txt")


def test_from_file_empty(tmp_path: Any) -> None:
    f = tmp_path / "e.txt"
    f.write_text("   \n", encoding="utf-8")
    with pytest.raises(CredentialsError):
        mgr_config.from_file(f)


def test_from_env_and_missing() -> None:
    assert (
        mgr_config.from_env({"MGF_INFOMANIAK_TOKEN": "envtok"}).token.get_secret_value() == "envtok"
    )
    with pytest.raises(CredentialsError):
        mgr_config.from_env({})


def test_load_precedence(tmp_path: Any) -> None:
    f = tmp_path / "t.txt"
    f.write_text("filetok", encoding="utf-8")
    assert load_manager_credentials(token="explicit").token.get_secret_value() == "explicit"
    assert load_manager_credentials(token_file=f).token.get_secret_value() == "filetok"
    assert (
        load_manager_credentials(environ={"INFOMANIAK_API_TOKEN": "e"}).token.get_secret_value()
        == "e"
    )


# --- the real adapter, via the fake transport --------------------------------
def test_list_products() -> None:
    products = _live(_ROUTES).list_products()
    assert [p.id for p in products] == [743284, 2047496]
    assert all(isinstance(p, Product) for p in products)


def test_list_web_hostings_filters_to_hosting() -> None:
    hostings = _live(_ROUTES).list_web_hostings()
    assert [h.id for h in hostings] == [743284]


def test_get_web_hosting() -> None:
    wh = _live(_ROUTES).get_web_hosting(743284)
    assert wh.id == 743284
    assert wh.version == 2
    assert wh.is_zero_price


def test_find_web_hosting() -> None:
    wh = _live(_ROUTES).find_web_hosting("magogi.org")
    assert wh.id == 743284


def test_find_web_hosting_absent() -> None:
    with pytest.raises(ResourceNotFoundError):
        _live(_ROUTES).find_web_hosting("nope.example")


def test_list_and_get_site() -> None:
    mgr = _live(_ROUTES)
    sites = mgr.list_sites(743284)
    assert sites[0].main_fqdn == "magogi.org"
    assert sites[0].is_starter
    assert not sites[0].has_ssh
    site = mgr.get_site(743284, 1349857)
    assert site.id == 1349857
    assert site.directory == "/"


def test_list_dns_records() -> None:
    records = _live(_ROUTES).list_dns_records("magogi.org")
    assert records[0].type == "A"
    assert records[0].target == "128.65.195.180"


def test_404_maps_to_not_found() -> None:
    mgr = _live({"1/web_hostings/999": (404, _err("not_found", "gone"))})
    with pytest.raises(ResourceNotFoundError):
        mgr.get_web_hosting(999)


def test_api_error_maps_to_manager_error() -> None:
    mgr = _live({"1/products": (403, _err("not_authorized", "Not authorized."))})
    with pytest.raises(ManagerApiError) as exc:
        mgr.list_products()
    assert exc.value.status == 403
    assert exc.value.code == "not_authorized"


def test_non_json_body() -> None:
    creds = mgr_config.from_token("t")
    transport = FakeTransport()
    # override to return raw non-json bytes
    transport.get = lambda url, headers, timeout: HttpResponse(status=200, body=b"<html>")  # type: ignore[method-assign]
    with pytest.raises(ManagerApiError):
        InfomaniakManager(creds, transport=transport, retries=0).list_products()


def test_retry_then_success(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr("mgf.cloud.manager.infomaniak.time.sleep", lambda _s: None)
    transport = FakeTransport(script=[(503, _err("busy", "x")), (200, _ok(PRODUCTS))])
    mgr = InfomaniakManager(mgr_config.from_token("t"), transport=transport, retries=2)
    products = mgr.list_products()
    assert len(products) == 2
    assert len(transport.calls) == 2


def test_retry_exhausted(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr("mgf.cloud.manager.infomaniak.time.sleep", lambda _s: None)
    transport = FakeTransport(script=[(503, _err("busy", "x")), (503, _err("busy", "x"))])
    mgr = InfomaniakManager(mgr_config.from_token("t"), transport=transport, retries=1)
    with pytest.raises(ManagerApiError) as exc:
        mgr.list_products()
    assert exc.value.status == 503


def test_transport_exception_wrapped() -> None:
    class Raising:
        def get(self, url: str, headers: dict[str, str], timeout: float) -> HttpResponse:
            raise urllib.error.URLError("boom")

    mgr = InfomaniakManager(mgr_config.from_token("t"), transport=Raising(), retries=0)
    with pytest.raises(ManagerApiError):
        mgr.list_products()


def test_urllib_transport_rejects_non_https() -> None:
    with pytest.raises(ManagerApiError):
        UrllibTransport().get("http://api.infomaniak.com/1/products", {}, 1.0)


def test_http_response_is_frozen() -> None:
    resp = HttpResponse(status=200, body=b"{}")
    assert resp.status == 200
    with pytest.raises(Exception):  # noqa: B017 - frozen dataclass raises FrozenInstanceError
        resp.status = 500  # type: ignore[misc]


# --- the fake adapter --------------------------------------------------------
def test_fake_manager_surface() -> None:
    mgr = get_manager("fake")
    assert isinstance(mgr, FakeManager)
    assert mgr.name == "fake"
    assert len(mgr.list_products()) == 2
    assert mgr.list_web_hostings()[0].id == 743284
    assert mgr.get_web_hosting(743284).customer_name == "example.test"
    assert mgr.find_web_hosting("example.test").id == 743284
    sites = mgr.list_sites(743284)
    assert sites[0].is_starter
    assert mgr.get_site(743284, 1349857).id == 1349857
    assert mgr.list_dns_records("example.test")[0].type == "A"


def test_fake_manager_not_found() -> None:
    mgr = FakeManager()
    with pytest.raises(ResourceNotFoundError):
        mgr.get_web_hosting(0)
    with pytest.raises(ResourceNotFoundError):
        mgr.find_web_hosting("absent")
    with pytest.raises(ResourceNotFoundError):
        mgr.list_sites(0)
    with pytest.raises(ResourceNotFoundError):
        mgr.get_site(743284, 0)
    with pytest.raises(ResourceNotFoundError):
        mgr.list_dns_records("absent")


# --- factory -----------------------------------------------------------------
def test_get_manager_factory() -> None:
    assert available_managers() == AVAILABLE_MANAGERS
    assert get_manager("fake").name == "fake"
    assert isinstance(get_manager("infomaniak", mgr_config.from_token("t")), InfomaniakManager)
    with pytest.raises(CredentialsError):
        get_manager("infomaniak")
    with pytest.raises(ProviderNotFoundError):
        get_manager("bogus")


# --- the CLI verb ------------------------------------------------------------
def _ns(**kw: Any) -> argparse.Namespace:
    base = {"manager_provider": "fake", "token_file": None, "as_json": False}
    base.update(kw)
    return argparse.Namespace(**base)


@pytest.mark.parametrize("as_json", [False, True])
def test_cli_manager_verbs(as_json: bool, capsys: Any) -> None:
    assert run_manager(_ns(action="products", as_json=as_json)) == 0
    assert run_manager(_ns(action="hostings", as_json=as_json)) == 0
    assert run_manager(_ns(action="hosting", hosting_id=743284, as_json=as_json)) == 0
    assert run_manager(_ns(action="sites", hosting_id=743284, as_json=as_json)) == 0
    assert run_manager(_ns(action="dns", fqdn="example.test", as_json=as_json)) == 0
    out = capsys.readouterr().out
    assert "starter" in out or "1349857" in out


def test_cli_resolve_infomaniak_branch(tmp_path: Any) -> None:
    from mgf.cloud.cli._manager import _resolve_manager

    f = tmp_path / "tok.txt"
    f.write_text("token: LIVE\n", encoding="utf-8")
    mgr = _resolve_manager(_ns(manager_provider="infomaniak", token_file=str(f)))
    assert isinstance(mgr, InfomaniakManager)
