from __future__ import annotations

import importlib.util
import os
from pathlib import Path
import subprocess
import tempfile
from types import ModuleType
from types import SimpleNamespace
import unittest

from unuforge.consumer_contracts import OFFICIAL_CONSUMERS, default_consumer_repo_root


ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "verify_consumer_contracts.py"
CONTRACT_DOC = ROOT / "docs" / "consumer-contract.md"
README = ROOT / "README.md"
RELEASING_GUIDE = ROOT / "docs" / "releasing.md"
PORTFOLIO_ADOPTION = ROOT / "docs" / "portfolio-adoption.md"
RELEASE_POLICY = ROOT / "docs" / "release-policy.md"


def _load_script_module() -> ModuleType:
    spec = importlib.util.spec_from_file_location("verify_consumer_contracts", SCRIPT)
    if spec is None or spec.loader is None:
        raise RuntimeError(f"Unable to load module from {SCRIPT}")
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


class ConsumerContractTests(unittest.TestCase):
    def test_official_consumers_match_current_samples(self) -> None:
        self.assertEqual(set(OFFICIAL_CONSUMERS), {"unudispatch", "unuidentity", "unuvault"})

        unudispatch = OFFICIAL_CONSUMERS["unudispatch"]
        self.assertEqual(unudispatch.code, "unudispatch")
        self.assertEqual(unudispatch.repo_dir_name, "unudispatch")
        self.assertEqual(
            unudispatch.preset_relpath,
            Path("presets/unudispatch/release-preset.json"),
        )

        unuidentity = OFFICIAL_CONSUMERS["unuidentity"]
        self.assertEqual(unuidentity.code, "unuidentity")
        self.assertEqual(unuidentity.repo_dir_name, "unuidentity")
        self.assertEqual(
            unuidentity.preset_relpath,
            Path("presets/unuidentity/phase0-bootstrap-preset.json"),
        )

        unuvault = OFFICIAL_CONSUMERS["unuvault"]
        self.assertEqual(unuvault.code, "unuvault")
        self.assertEqual(unuvault.repo_dir_name, "unuvault")
        self.assertEqual(
            unuvault.preset_relpath,
            Path("presets/unuvault/release-preset.json"),
        )

    def test_unudispatch_command_templates_match_executable_profile_contract(self) -> None:
        consumer = OFFICIAL_CONSUMERS["unudispatch"]

        rendered = consumer.render_commands(
            consumer_repo_root=Path("/tmp/unudispatch"),
            unuforge_repo_root=Path("/tmp/unuforge"),
        )

        self.assertEqual(
            [command.label for command in rendered],
            [
                "preset-inspect",
                "profiles-list",
                "profiles-run-lint-runner-dry-run",
                "profiles-run-test-runner-dry-run",
            ],
        )
        self.assertEqual(
            rendered[0].argv,
            (
                "python3",
                "-m",
                "unuforge.cli",
                "preset",
                "inspect",
                "--preset",
                "/tmp/unudispatch/presets/unudispatch/release-preset.json",
                "--json",
            ),
        )
        self.assertEqual(
            rendered[2].argv,
            (
                "python3",
                "-m",
                "unuforge.cli",
                "profiles",
                "run",
                "lint-runner",
                "--preset",
                "/tmp/unudispatch/presets/unudispatch/release-preset.json",
                "--host-adapter",
                "unudispatch_forge_host",
                "--dry-run",
                "--json",
            ),
        )
        self.assertEqual(rendered[3].env["UNUFORGE_REPO_ROOT"], "/tmp/unuforge")

    def test_unuidentity_command_templates_match_bootstrap_contract(self) -> None:
        consumer = OFFICIAL_CONSUMERS["unuidentity"]

        rendered = consumer.render_commands(
            consumer_repo_root=Path("/tmp/unuidentity"),
            unuforge_repo_root=Path("/tmp/unuforge"),
        )

        self.assertEqual(
            [command.label for command in rendered],
            [
                "preset-inspect",
                "profiles-list",
                "profiles-run-identity-env-template-dry-run",
                "actions-run-identity-bootstrap-apply-dry-run",
                "actions-run-identity-bootstrap-check-dry-run",
            ],
        )
        self.assertEqual(
            rendered[2].argv,
            (
                "python3",
                "-m",
                "unuforge.cli",
                "profiles",
                "run",
                "identity.env-template",
                "--preset",
                "/tmp/unuidentity/presets/unuidentity/phase0-bootstrap-preset.json",
                "--host-adapter",
                "unuidentity_forge_host",
                "--dry-run",
                "--json",
            ),
        )
        self.assertEqual(
            rendered[3].argv,
            (
                "python3",
                "-m",
                "unuforge.cli",
                "actions",
                "run",
                "identity.bootstrap.apply",
                "--preset",
                "/tmp/unuidentity/presets/unuidentity/phase0-bootstrap-preset.json",
                "--host-adapter",
                "unuidentity_forge_host",
                "--dry-run",
                "--json",
                "--",
                "--dry-run",
            ),
        )
        self.assertEqual(rendered[4].env["UNUFORGE_REPO_ROOT"], "/tmp/unuforge")

    def test_unuvault_command_templates_match_js_safe_profile_contract(self) -> None:
        consumer = OFFICIAL_CONSUMERS["unuvault"]

        rendered = consumer.render_commands(
            consumer_repo_root=Path("/tmp/unuvault"),
            unuforge_repo_root=Path("/tmp/unuforge"),
        )

        self.assertEqual(
            [command.label for command in rendered],
            [
                "preset-inspect",
                "profiles-list",
                "profiles-run-lint-runner-dry-run",
                "profiles-run-test-runner-dry-run",
            ],
        )
        self.assertEqual(
            rendered[2].argv,
            (
                "python3",
                "-m",
                "unuforge.cli",
                "profiles",
                "run",
                "lint-runner",
                "--preset",
                "/tmp/unuvault/presets/unuvault/release-preset.json",
                "--host-adapter",
                "unuvault_forge_host",
                "--dry-run",
                "--json",
            ),
        )
        self.assertEqual(
            rendered[3].argv,
            (
                "python3",
                "-m",
                "unuforge.cli",
                "profiles",
                "run",
                "test-runner",
                "--preset",
                "/tmp/unuvault/presets/unuvault/release-preset.json",
                "--host-adapter",
                "unuvault_forge_host",
                "--dry-run",
                "--json",
            ),
        )
        self.assertEqual(rendered[3].env["UNUFORGE_REPO_ROOT"], "/tmp/unuforge")

    def test_default_consumer_repo_root_uses_parent_of_standard_checkout(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            temp_root = Path(tmp_dir)
            repo_root = temp_root / "portfolio" / "unuforge"
            (repo_root / ".git").mkdir(parents=True)

            self.assertEqual(
                default_consumer_repo_root(
                    OFFICIAL_CONSUMERS["unudispatch"],
                    unuforge_repo_root=repo_root,
                ),
                (temp_root / "portfolio" / "unudispatch").resolve(),
            )

    def test_default_consumer_repo_root_uses_common_checkout_parent_for_worktree(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            temp_root = Path(tmp_dir)
            common_repo_root = temp_root / "portfolio" / "unuforge"
            common_git_dir = common_repo_root / ".git" / "worktrees" / "unuforge-feature"
            worktree_root = temp_root / "worktrees" / "unuforge-feature"

            common_git_dir.mkdir(parents=True)
            worktree_root.mkdir(parents=True)
            (worktree_root / ".git").write_text(
                f"gitdir: {common_git_dir}\n",
                encoding="utf-8",
            )

            self.assertEqual(
                default_consumer_repo_root(
                    OFFICIAL_CONSUMERS["unuidentity"],
                    unuforge_repo_root=worktree_root,
                ),
                (temp_root / "portfolio" / "unuidentity").resolve(),
            )

    def test_script_can_select_all_or_one_consumer(self) -> None:
        module = _load_script_module()

        self.assertEqual(
            [consumer.code for consumer in module.resolve_selected_consumers("all")],
            ["unudispatch", "unuidentity", "unuvault"],
        )
        self.assertEqual(
            [consumer.code for consumer in module.resolve_selected_consumers("unuidentity")],
            ["unuidentity"],
        )
        self.assertEqual(
            [consumer.code for consumer in module.resolve_selected_consumers("unuvault")],
            ["unuvault"],
        )

    def test_script_formats_readable_command_preview(self) -> None:
        module = _load_script_module()
        rendered = OFFICIAL_CONSUMERS["unudispatch"].render_commands(
            consumer_repo_root=Path("/tmp/unudispatch"),
            unuforge_repo_root=Path("/tmp/unuforge"),
        )[0]

        preview = module.format_command_preview(rendered)

        self.assertEqual(
            preview,
            "UNUFORGE_REPO_ROOT=/tmp/unuforge python3 -m unuforge.cli preset inspect "
            "--preset /tmp/unudispatch/presets/unudispatch/release-preset.json --json",
        )

    def test_script_resolves_explicit_repo_root_override(self) -> None:
        module = _load_script_module()

        resolved = module.resolve_consumer_repo_root(
            OFFICIAL_CONSUMERS["unudispatch"],
            env={"UNUDISPATCH_REPO_ROOT": "/tmp/custom-unudispatch"},
            unuforge_repo_root=ROOT,
        )

        self.assertEqual(resolved, Path("/tmp/custom-unudispatch"))

    def test_script_runs_rendered_commands_with_unuforge_repo_root(self) -> None:
        module = _load_script_module()
        calls: list[tuple[tuple[str, ...], Path, dict[str, str]]] = []

        def fake_runner(
            argv: tuple[str, ...],
            *,
            cwd: Path,
            env: dict[str, str],
        ) -> SimpleNamespace:
            calls.append((argv, cwd, env))
            return SimpleNamespace(returncode=0, stdout="", stderr="")

        module.run_consumer_contract(
            OFFICIAL_CONSUMERS["unuidentity"],
            consumer_repo_root=Path("/tmp/unuidentity"),
            unuforge_repo_root=Path("/tmp/unuforge"),
            runner=fake_runner,
        )

        self.assertEqual(len(calls), 5)
        self.assertEqual(calls[0][1], Path("/tmp/unuidentity"))
        self.assertEqual(calls[0][2]["UNUFORGE_REPO_ROOT"], "/tmp/unuforge")
        self.assertEqual(
            calls[-1][0],
            (
                "python3",
                "-m",
                "unuforge.cli",
                "actions",
                "run",
                "identity.bootstrap.check",
                "--preset",
                "/tmp/unuidentity/presets/unuidentity/phase0-bootstrap-preset.json",
                "--host-adapter",
                "unuidentity_forge_host",
                "--dry-run",
                "--json",
                "--",
                "--dry-run",
            ),
        )

    def test_script_can_start_as_direct_python_entrypoint(self) -> None:
        env = os.environ.copy()
        env.pop("PYTHONPATH", None)
        env.pop("PYTHONHOME", None)
        completed = subprocess.run(
            ["python3", str(SCRIPT), "--help"],
            cwd=ROOT,
            env=env,
            capture_output=True,
            text=True,
        )

        self.assertEqual(completed.returncode, 0, completed.stderr)
        self.assertIn("Run official repo-local consumer contract dry-run verification.", completed.stdout)
        self.assertIn("unuvault", completed.stdout)

    def test_docs_define_official_consumer_contract_examples(self) -> None:
        self.assertTrue(CONTRACT_DOC.exists(), CONTRACT_DOC)
        contract_text = CONTRACT_DOC.read_text(encoding="utf-8")

        self.assertIn("unudispatch", contract_text)
        self.assertIn("unuidentity", contract_text)
        self.assertIn("unuvault", contract_text)
        self.assertIn("sibling checkout", contract_text)
        self.assertIn("UNUDISPATCH_REPO_ROOT", contract_text)
        self.assertIn("UNUIDENTITY_REPO_ROOT", contract_text)
        self.assertIn("UNUVAULT_REPO_ROOT", contract_text)
        self.assertIn("python3 scripts/verify_consumer_contracts.py --consumer all", contract_text)
        self.assertIn("installed-package", contract_text)

    def test_readme_and_release_guide_link_consumer_contract_verification(self) -> None:
        readme_text = README.read_text(encoding="utf-8")
        release_text = RELEASING_GUIDE.read_text(encoding="utf-8")
        normalized_readme = " ".join(readme_text.split())
        normalized_release = " ".join(release_text.split())

        self.assertIn("docs/consumer-contract.md", readme_text)
        self.assertIn("python3 scripts/verify_consumer_contracts.py --consumer all", readme_text)
        self.assertIn("CROSS_REPO_READ_TOKEN", readme_text)
        self.assertIn("UNUDISPATCH_REPO_ROOT", readme_text)
        self.assertIn("UNUIDENTITY_REPO_ROOT", readme_text)
        self.assertIn("fixture/stub-host", readme_text)
        self.assertIn("consumer-contract", readme_text)
        self.assertIn("same consumer contract verification command", normalized_readme)
        self.assertIn("scripts/verify_consumer_contracts.py --consumer all", release_text)
        self.assertIn("CROSS_REPO_READ_TOKEN", release_text)
        self.assertIn("UNUDISPATCH_REPO_ROOT", release_text)
        self.assertIn("UNUIDENTITY_REPO_ROOT", release_text)
        self.assertIn("downstream consumer contract verification", release_text)
        self.assertIn("same consumer contract verification command", normalized_release)

    def test_rollout_and_policy_docs_position_unundo_as_exemplar_not_official_sample(self) -> None:
        portfolio_text = PORTFOLIO_ADOPTION.read_text(encoding="utf-8")
        release_policy_text = RELEASE_POLICY.read_text(encoding="utf-8")

        self.assertIn("deep-adoption exemplar", portfolio_text)
        self.assertIn("not part of the blocking official sample baseline", portfolio_text)
        self.assertIn("installed-package gate", portfolio_text)
        self.assertIn("installed-package validation should be evaluated separately", release_policy_text)
        self.assertIn("unundo", release_policy_text)
        self.assertIn("deep-adoption exemplar", release_policy_text)

    def test_docs_define_installed_package_gate_as_release_mandatory_and_main_advisory(self) -> None:
        portfolio_text = PORTFOLIO_ADOPTION.read_text(encoding="utf-8")
        release_policy_text = RELEASE_POLICY.read_text(encoding="utf-8")
        release_guide_text = RELEASING_GUIDE.read_text(encoding="utf-8")

        self.assertIn("advisory on `main`", release_policy_text)
        self.assertIn("mandatory on release", release_policy_text)
        self.assertIn("unuidentity", release_policy_text)
        self.assertIn("unundo", release_policy_text)
        self.assertIn("initial installed-package gate repos", portfolio_text)
        self.assertIn("unuidentity", portfolio_text)
        self.assertIn("unundo", portfolio_text)
        self.assertIn("unuvault", portfolio_text)
        self.assertIn("outside that gate", portfolio_text)
        self.assertIn("installed-package downstream gate", release_guide_text)
        self.assertIn("verify_installed_package_consumers.py", release_guide_text)

    def test_script_formats_missing_repo_message_with_override_hint(self) -> None:
        module = _load_script_module()

        message = module.format_missing_repo_message(
            OFFICIAL_CONSUMERS["unudispatch"],
            Path("/tmp/missing-unudispatch"),
        )

        self.assertIn("/tmp/missing-unudispatch", message)
        self.assertIn("UNUDISPATCH_REPO_ROOT", message)
        self.assertIn("sibling checkout", message)


if __name__ == "__main__":
    unittest.main()
