"""Tests for INTEGRATION_REGISTRY — mechanics, completeness, and registrar alignment."""

import json
import os
from pathlib import PurePosixPath

import pytest
from typer.testing import CliRunner

from specify_cli import app
from specify_cli.integrations import (
    INTEGRATION_REGISTRY,
    _register,
    get_integration,
)
from specify_cli.integrations.base import MarkdownIntegration
from .conftest import StubIntegration


# Every integration key that must be registered (Stage 2 + Stage 3 + Stage 4 + Stage 5).
ALL_INTEGRATION_KEYS = [
    "copilot",
    # Stage 3 — standard markdown integrations
    "claude", "qwen", "opencode", "junie", "kilocode", "auggie",
    "rovodev", "codebuddy", "qodercli", "amp", "shai", "bob", "trae",
    "pi", "kiro-cli", "vibe", "cursor-agent", "firebender",
    # Stage 4 — TOML integrations
    "gemini", "tabnine",
    # Stage 5 — skills, generic & option-driven integrations
    "codex", "kimi", "agy", "zed", "generic",
]


def _multi_install_safe_keys() -> list[str]:
    return sorted(
        key
        for key, integration in INTEGRATION_REGISTRY.items()
        if integration.multi_install_safe
    )


def _multi_install_safe_pairs() -> list[tuple[str, str]]:
    safe_keys = _multi_install_safe_keys()
    return [
        (safe_keys[left], safe_keys[right])
        for left in range(len(safe_keys))
        for right in range(left + 1, len(safe_keys))
    ]


def _multi_install_safe_orders() -> list[list[str]]:
    safe_keys = _multi_install_safe_keys()
    if len(safe_keys) < 2:
        return [safe_keys]
    return [safe_keys[index:] + safe_keys[:index] for index in range(len(safe_keys))]


def _multi_install_safe_order_id(ordered_keys: list[str]) -> str:
    if not ordered_keys:
        return "no-safe-integrations"
    return f"init-{ordered_keys[0]}"


def _posix_path(value: str | None) -> str | None:
    if not value:
        return None
    return PurePosixPath(value).as_posix()


def _integration_root_dir(key: str) -> str | None:
    integration = INTEGRATION_REGISTRY[key]
    cfg = integration.config if isinstance(integration.config, dict) else {}
    return _posix_path(cfg.get("folder"))


def _integration_commands_dir(key: str) -> str | None:
    integration = INTEGRATION_REGISTRY[key]
    cfg = integration.config if isinstance(integration.config, dict) else {}
    folder = cfg.get("folder")
    if not folder:
        return None
    subdir = cfg.get("commands_subdir", "commands")
    return (PurePosixPath(folder) / subdir).as_posix()


def _paths_overlap(first: str | None, second: str | None) -> bool:
    if not first or not second:
        return False
    left = PurePosixPath(first)
    right = PurePosixPath(second)
    try:
        left.relative_to(right)
        return True
    except ValueError:
        pass
    try:
        right.relative_to(left)
        return True
    except ValueError:
        return False


class TestRegistry:
    def test_registry_is_dict(self):
        assert isinstance(INTEGRATION_REGISTRY, dict)

    def test_register_and_get(self):
        stub = StubIntegration()
        _register(stub)
        try:
            assert get_integration("stub") is stub
        finally:
            INTEGRATION_REGISTRY.pop("stub", None)

    def test_get_missing_returns_none(self):
        assert get_integration("nonexistent-xyz") is None

    def test_register_empty_key_raises(self):
        class EmptyKey(MarkdownIntegration):
            key = ""
        with pytest.raises(ValueError, match="empty key"):
            _register(EmptyKey())

    def test_register_duplicate_raises(self):
        stub = StubIntegration()
        _register(stub)
        try:
            with pytest.raises(KeyError, match="already registered"):
                _register(StubIntegration())
        finally:
            INTEGRATION_REGISTRY.pop("stub", None)


class TestRegistryCompleteness:
    """Every expected integration must be registered."""

    @pytest.mark.parametrize("key", ALL_INTEGRATION_KEYS)
    def test_key_registered(self, key):
        assert key in INTEGRATION_REGISTRY, f"{key} missing from registry"


class TestRegistrarKeyAlignment:
    """Every integration key must have a matching AGENT_CONFIGS entry.

    ``generic`` is excluded because it has no fixed directory — its
    output path comes from ``--commands-dir`` at runtime.
    """

    @pytest.mark.parametrize(
        "key",
        [k for k in ALL_INTEGRATION_KEYS if k != "generic"],
    )
    def test_integration_key_in_registrar(self, key):
        from specify_cli.agents import CommandRegistrar
        assert key in CommandRegistrar.AGENT_CONFIGS, (
            f"Integration '{key}' is registered but has no AGENT_CONFIGS entry"
        )

    def test_no_stale_cursor_shorthand(self):
        """The old 'cursor' shorthand must not appear in AGENT_CONFIGS."""
        from specify_cli.agents import CommandRegistrar
        assert "cursor" not in CommandRegistrar.AGENT_CONFIGS


class TestMultiInstallSafeContracts:
    """Declared safe integrations must stay isolated from each other."""

    def test_safe_install_orders_rotate_each_integration_through_init(self):
        safe_keys = _multi_install_safe_keys()
        orders = _multi_install_safe_orders()

        assert len(safe_keys) >= 2
        assert [order[0] for order in orders] == safe_keys
        assert len({tuple(order) for order in orders}) == len(safe_keys)
        assert all(sorted(order) == safe_keys for order in orders)

    @pytest.mark.parametrize("key", _multi_install_safe_keys())
    def test_safe_integrations_have_static_isolated_paths(self, key):
        assert _integration_root_dir(key), (
            f"{key} is declared multi-install safe but has no static root directory"
        )
        assert _integration_commands_dir(key), (
            f"{key} is declared multi-install safe but has no static commands directory"
        )

    @pytest.mark.parametrize(("first", "second"), _multi_install_safe_pairs())
    def test_safe_integrations_have_distinct_agent_roots(self, first, second):
        assert not _paths_overlap(_integration_root_dir(first), _integration_root_dir(second)), (
            f"{first} and {second} are declared multi-install safe but have "
            f"overlapping agent roots {_integration_root_dir(first)!r} and "
            f"{_integration_root_dir(second)!r}"
        )

    @pytest.mark.parametrize(("first", "second"), _multi_install_safe_pairs())
    def test_safe_integrations_have_distinct_command_dirs(self, first, second):
        assert not _paths_overlap(_integration_commands_dir(first), _integration_commands_dir(second)), (
            f"{first} and {second} are declared multi-install safe but have "
            f"overlapping command directories {_integration_commands_dir(first)!r} and "
            f"{_integration_commands_dir(second)!r}"
        )

    @pytest.mark.parametrize(
        "ordered_keys",
        _multi_install_safe_orders(),
        ids=_multi_install_safe_order_id,
    )
    def test_safe_integrations_have_disjoint_manifests(
        self,
        tmp_path,
        ordered_keys,
    ):
        # The pairwise disjointness contract is only meaningful with at least
        # two safe integrations. Guard so a shrunken registry fails loudly here
        # rather than passing vacuously (or tripping over ordered_keys[0] below).
        assert len(ordered_keys) >= 2, (
            f"expected at least two multi-install-safe integrations, got {ordered_keys}"
        )

        project_root = tmp_path / "project"
        project_root.mkdir()
        runner = CliRunner()

        # Install every safe integration once into a single project, then assert
        # pairwise manifest isolation. Each safe integration writes only to its
        # own (disjoint) directories and always records what it writes, so a
        # manifest's contents are independent of install order and of which other
        # integrations are co-installed. The parametrized rotations keep the
        # aggregate setup while placing each safe integration first once, so each
        # one still exercises the `specify init --integration ...` path.
        original_cwd = os.getcwd()
        try:
            os.chdir(project_root)
            init_result = runner.invoke(
                app,
                [
                    "init",
                    "--here",
                    "--integration",
                    ordered_keys[0],
                    "--script",
                    "sh",
                    "--ignore-agent-tools",
                ],
                catch_exceptions=False,
            )
            assert init_result.exit_code == 0, init_result.output

            for key in ordered_keys[1:]:
                install_result = runner.invoke(
                    app,
                    ["integration", "install", key, "--script", "sh"],
                    catch_exceptions=False,
                )
                assert install_result.exit_code == 0, install_result.output
        finally:
            os.chdir(original_cwd)

        integrations_dir = project_root / ".specify" / "integrations"
        manifests = {}
        for key in ordered_keys:
            manifest = json.loads(
                (integrations_dir / f"{key}.manifest.json").read_text(encoding="utf-8")
            )
            files = manifest.get("files", {})
            assert isinstance(files, dict), f"{key} manifest files must be an object"
            manifests[key] = set(files.keys())

        for first, second in _multi_install_safe_pairs():
            overlap = manifests[first] & manifests[second]
            assert not overlap, (
                f"{first} and {second} are declared multi-install safe but both manage "
                f"these files: {sorted(overlap)}"
            )


class TestCatalogParity:
    """The discovery catalog must list every registered integration."""

    def test_every_registered_integration_is_in_catalog(self):
        """``integrations/catalog.json`` must cover every registry key.

        The catalog is the discovery manifest; an integration that is
        registered, registrar-aligned and registry-tested but missing from
        the catalog is undiscoverable through it. ``generic`` is exempt —
        it is the no-fixed-directory fallback, not a catalogued agent.
        """
        from pathlib import Path

        repo_root = Path(__file__).resolve().parents[2]
        catalog = json.loads(
            (repo_root / "integrations" / "catalog.json").read_text(encoding="utf-8")
        )
        catalogued = set(catalog["integrations"])
        registered = set(INTEGRATION_REGISTRY) - {"generic"}
        missing = sorted(registered - catalogued)
        assert not missing, f"integrations missing from catalog.json: {missing}"
