"""Run every story's ``main`` over the in-process (transport × era × variant) matrix."""

from __future__ import annotations

import importlib
import inspect
from typing import Any

import anyio
import pytest

from tests.examples.conftest import MANIFEST, STORIES, STORIES_DIR, Hosted, Leg, story_cfg

pytestmark = pytest.mark.anyio


async def test_story(leg: Leg, cfg: dict[str, Any], hosted: Hosted, client_module: Any) -> None:
    kwargs: dict[str, Any] = {"mode": leg.mode}
    if cfg["needs_http"]:
        kwargs["http"] = hosted.http
    with anyio.fail_after(cfg["timeout_s"]):
        if cfg["multi_connection"]:
            await client_module.main(hosted.targets, **kwargs)
        else:
            await client_module.main(hosted.targets(), **kwargs)


def test_manifest_matches_filesystem() -> None:
    """Manifest [story.*] / [deferred] keys and on-disk story directories agree exactly."""
    dirs = {d.name for d in STORIES_DIR.iterdir() if d.is_dir() and not d.name.startswith(("_", "."))}
    runnable = {d for d in dirs if (STORIES_DIR / d / "client.py").exists()}
    in_manifest = set(STORIES)
    assert runnable == in_manifest, {"only_on_disk": runnable - in_manifest, "only_in_manifest": in_manifest - runnable}
    # README-only stub dirs must be exactly the [deferred] table.
    deferred_manifest = set(MANIFEST.get("deferred", {}))
    assert dirs - runnable == deferred_manifest, {
        "stub_dirs_missing_from_manifest": (dirs - runnable) - deferred_manifest,
        "deferred_entries_missing_dir": deferred_manifest - (dirs - runnable),
    }
    assert runnable.isdisjoint(deferred_manifest), "deferred stories must not have a client.py"


_ERAS = {"dual", "modern", "legacy", "dual-in-body"}
_TRANSPORTS = {"in-memory", "http-asgi"}
_SERVER_EXPORTS = {"factory", "app"}


def test_manifest_schema_valid() -> None:
    """Declared manifest values are mutually consistent with the story files."""
    for name in STORIES:
        cfg = story_cfg(name)
        assert "-" not in name, f"{name!r}: story directories must be underscored"
        assert cfg["era"] in _ERAS, f"{name!r}: era={cfg['era']!r} not in {_ERAS}"
        assert cfg["server_export"] in _SERVER_EXPORTS, f"{name!r}: server_export={cfg['server_export']!r}"
        assert set(cfg["transports"]) <= _TRANSPORTS, f"{name!r}: transports={cfg['transports']!r}"
        assert (STORIES_DIR / name / "__init__.py").exists(), f"{name!r}: missing __init__.py"
        if cfg["server_export"] == "factory":
            assert (STORIES_DIR / name / "server.py").exists(), f"{name!r}: missing server.py"
        else:
            assert "in-memory" not in cfg["transports"], f"{name!r}: server_export='app' cannot run in-memory"
        if cfg["needs_http"]:
            assert cfg["transports"] == ["http-asgi"], f"{name!r}: needs_http requires transports=['http-asgi']"
        ll = STORIES_DIR / name / "server_lowlevel.py"
        assert cfg["lowlevel"] == ll.exists(), f"{name!r}: lowlevel={cfg['lowlevel']} vs server_lowlevel.py on disk"


@pytest.mark.parametrize("name", sorted(STORIES))
def test_main_signature_matches_manifest(name: str) -> None:
    """``main``'s first parameter is ``target``/``targets`` per ``multi_connection``; ``http`` iff ``needs_http``."""
    cfg = story_cfg(name)
    params = list(inspect.signature(importlib.import_module(f"stories.{name}.client").main).parameters)
    first = "targets" if cfg["multi_connection"] else "target"
    assert params[0] == first, f"{name}: first param is {params[0]!r}, expected {first!r}"
    assert ("http" in params) == cfg["needs_http"], f"{name}: 'http' param vs needs_http={cfg['needs_http']}"
