"""E2E test harness: live HTTP client + API-coverage recorder.

Coverage is measured against docs/openapi.yaml. Each E2E test issues a real
request and, on a passing status assertion, records the (method, path, status)
triple it covered. At session end the recorder writes API_COVERAGE_FILE
(default qa-coverage/api_coverage.json) in the schema the runner understands.
"""

import json
import os
import pathlib

import pytest
import requests
import yaml

REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
SPEC_PATH = REPO_ROOT / "docs" / "openapi.yaml"
OUT_DIR = pathlib.Path(os.environ.get("OUT_DIR", "qa-coverage"))
COVERAGE_FILE = OUT_DIR / "api_coverage.json"


def _load_spec_triples():
    """Returns (all_triples, external_triples).

    A triple is `external` when its OpenAPI response is tagged
    `x-requires-external: true` — its success path needs a live backend, so it
    is excluded from the coverage denominator (reported separately).
    """
    spec = yaml.safe_load(SPEC_PATH.read_text())
    triples, external = set(), set()
    for path, ops in spec["paths"].items():
        for method, op in ops.items():
            if method.startswith("x-") or not isinstance(op, dict):
                continue
            for status, resp in op.get("responses", {}).items():
                t = (method.upper(), path, str(status))
                triples.add(t)
                if isinstance(resp, dict) and resp.get("x-requires-external"):
                    external.add(t)
    return triples, external


class ApiCoverage:
    def __init__(self):
        self.all_triples, self.external_triples = _load_spec_triples()
        # In-scope = everything not flagged x-requires-external.
        self.scope_triples = self.all_triples - self.external_triples
        self.scope_endpoints = {(m, p) for m, p, _ in self.scope_triples}
        self.hit_endpoints = set()
        self.hit_triples = set()

    def record(self, method, spec_path, status):
        self.hit_endpoints.add((method.upper(), spec_path))
        self.hit_triples.add((method.upper(), spec_path, str(status)))

    def summary(self):
        ep_total = len(self.scope_endpoints)
        st_total = len(self.scope_triples)
        ep_hit = len(self.hit_endpoints & self.scope_endpoints)
        st_hit = len(self.hit_triples & self.scope_triples)
        missing_ep = sorted(f"{m} {p}" for m, p in self.scope_endpoints - self.hit_endpoints)
        missing_st = sorted(f"{m} {p} -> {s}" for m, p, s in self.scope_triples - self.hit_triples)
        excluded = sorted(f"{m} {p} -> {s}" for m, p, s in self.external_triples)
        return {
            # Coverage is measured over the in-scope set (external triples
            # excluded — see `excluded_external`).
            "summary": {
                "endpoint": {
                    "hit": ep_hit,
                    "total": ep_total,
                    "pct": round(ep_hit / ep_total * 100, 2) if ep_total else 0.0,
                },
                "status": {
                    "hit": st_hit,
                    "total": st_total,
                    "pct": round(st_hit / st_total * 100, 2) if st_total else 0.0,
                },
            },
            "missing": {"endpoints": missing_ep, "statuses": missing_st},
            "excluded_external": excluded,
        }


@pytest.fixture(scope="session")
def base_url():
    return os.environ.get("E2E_BASE_URL", "http://localhost:8000").rstrip("/")


@pytest.fixture(scope="session")
def _coverage():
    cov = ApiCoverage()
    yield cov
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    COVERAGE_FILE.write_text(json.dumps(cov.summary(), indent=2))


@pytest.fixture(scope="session", autouse=True)
def _warm_server(base_url):
    """Initialise the server's shared APIFunctions singleton.

    A couple of FOS handlers read `self.access_key` (set lazily by `set_env`),
    so the very first FOS call on a cold server raises → HTTP 500. Hitting one
    `set_env` endpoint first makes the FOS HTTP contract deterministic — this
    mirrors normal usage, where the UI always selects an environment first.
    """
    try:
        requests.get(f"{base_url}/list_objects/_cqa_warmup", timeout=30)
    except requests.RequestException:
        pass


@pytest.fixture
def api(base_url, _coverage):
    """Client that asserts a status and records the covered spec triple."""
    session = requests.Session()

    class _Client:
        def check(self, method, spec_path, concrete_path, expected, **kwargs):
            url = base_url + concrete_path
            resp = session.request(method, url, timeout=15, **kwargs)
            assert resp.status_code == expected, (
                f"{method} {concrete_path} expected {expected}, "
                f"got {resp.status_code}: {resp.text[:200]}"
            )
            _coverage.record(method, spec_path, expected)
            return resp

    yield _Client()
    session.close()
