"""Docker topology — normalization + error handling against the socket-proxy."""

from __future__ import annotations

import pytest

from psyc.cockpit import docker_view


_CONTAINERS_FIXTURE = [
    {
        "Id": "abcdef1234567890",
        "Names": ["/psyc-cockpit-1"],
        "Image": "psyc:latest",
        "State": "running",
        "Status": "Up 5 minutes (healthy)",
        "NetworkSettings": {"Networks": {"backend": {"IPAddress": "172.20.0.5"}}},
        "Ports": [{"IP": "0.0.0.0", "PrivatePort": 8767, "PublicPort": 8767, "Type": "tcp"}],
    },
    {
        "Id": "fedcba0987654321",
        "Names": ["/some-stopped"],
        "Image": "alpine",
        "State": "exited",
        "Status": "Exited (0) 2 hours ago",
        "NetworkSettings": {"Networks": {}},
        "Ports": [],
    },
]

_NETWORKS_FIXTURE = [
    {
        "Id": "n1", "Name": "backend", "Driver": "bridge", "Scope": "local", "Internal": False,
        "IPAM": {"Config": [{"Subnet": "172.20.0.0/16", "Gateway": "172.20.0.1"}]},
        "Containers": {
            "abcdef1234567890": {
                "Name": "psyc-cockpit-1", "IPv4Address": "172.20.0.5/16",
                "MacAddress": "02:42:ac:14:00:05",
            },
        },
    },
    {"Id": "n2", "Name": "bridge", "Driver": "bridge", "Scope": "local", "Internal": False, "Containers": {}},
]


def _fake_get_factory(monkeypatch, payloads: dict):
    """Patch docker_view._get to return canned payloads by path.

    Unknown paths raise DockerProxyError (mimicking a proxy that blocks the
    endpoint), so callers like host_info() exercise their fallback paths.
    """
    def fake_get(path: str):
        for prefix, body in payloads.items():
            if path.startswith(prefix):
                return body
        raise docker_view.DockerProxyError(f"blocked: {path}")
    monkeypatch.setattr(docker_view, "_get", fake_get)


def test_list_containers_normalizes_fields(monkeypatch):
    _fake_get_factory(monkeypatch, {"/containers/json": _CONTAINERS_FIXTURE})
    out = docker_view.list_containers()
    by_name = {c["name"]: c for c in out}
    # running comes before exited
    assert out[0]["state"] == "running"
    assert out[-1]["state"] == "exited"
    cockpit = by_name["psyc-cockpit-1"]
    assert cockpit["id"] == "abcdef123456"
    assert cockpit["image"] == "psyc:latest"
    assert cockpit["networks"][0]["name"] == "backend"
    assert cockpit["networks"][0]["ip"] == "172.20.0.5"
    assert "0.0.0.0:8767->8767/tcp" in cockpit["ports"]


def test_list_networks_attaches_containers_with_ip(monkeypatch):
    _fake_get_factory(monkeypatch, {"/networks": _NETWORKS_FIXTURE})
    out = docker_view.list_networks()
    backend = next(n for n in out if n["name"] == "backend")
    assert backend["driver"] == "bridge"
    assert backend["subnet"] == "172.20.0.0/16"
    assert backend["gateway"] == "172.20.0.1"
    assert backend["containers"][0]["name"] == "psyc-cockpit-1"
    assert backend["containers"][0]["ip"] == "172.20.0.5"
    assert backend["containers"][0]["mac"] == "02:42:ac:14:00:05"
    # default networks pushed to bottom
    assert out[-1]["name"] == "bridge"


def test_list_containers_extracts_published_ports(monkeypatch):
    _fake_get_factory(monkeypatch, {"/containers/json": _CONTAINERS_FIXTURE})
    out = docker_view.list_containers()
    cockpit = next(c for c in out if c["name"] == "psyc-cockpit-1")
    assert cockpit["published_ports"] == ["8767/tcp"]
    # stopped container with no ports → empty
    stopped = next(c for c in out if c["name"] == "some-stopped")
    assert stopped["published_ports"] == []


def test_host_info_falls_back_when_proxy_blocks_info(monkeypatch):
    def boom(path):
        raise docker_view.DockerProxyError("info forbidden")
    monkeypatch.setattr(docker_view, "_get", boom)
    info = docker_view.host_info()
    assert info["name"] == "docker host"


def test_topology_returns_both_with_no_error(monkeypatch):
    _fake_get_factory(monkeypatch, {
        "/containers/json": _CONTAINERS_FIXTURE,
        "/networks": _NETWORKS_FIXTURE,
    })
    snap = docker_view.topology()
    assert snap["error"] is None
    assert len(snap["containers"]) == 2
    assert len(snap["networks"]) == 2


def test_topology_surfaces_proxy_failure(monkeypatch):
    def boom(path):
        raise docker_view.DockerProxyError("connection refused")
    monkeypatch.setattr(docker_view, "_get", boom)
    snap = docker_view.topology()
    assert snap["error"] is not None and "connection refused" in snap["error"]
    assert snap["containers"] == [] and snap["networks"] == []
