#!/usr/bin/env python3
from __future__ import annotations

import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest

CHECKER = Path(__file__).with_name("framework-drift-check.py")
REAL_RESOLVER = CHECKER.parents[2] / "_lib" / "manifest.sh"


class FrameworkDriftCheckTests(unittest.TestCase):
    def setUp(self) -> None:
        self.temp = tempfile.TemporaryDirectory()
        root = Path(self.temp.name)
        self.framework = root / "framework"
        self.source = self.framework / "tools"
        self.installed = root / "home" / "tools"
        for directory in (self.source / "git", self.source / "_lib", self.installed / "git", self.installed / "_lib"):
            directory.mkdir(parents=True, exist_ok=True)
        shutil.copy2(REAL_RESOLVER, self.source / "_lib" / "manifest.sh")
        (self.source / "git" / "guard.sh").write_text("fixed\n")
        (self.source / "git" / "new-wrapper.sh").write_text("new\n")
        (self.source / "_lib" / "credentials.json").write_text("source-placeholder\n")
        self.write_manifest()

    def tearDown(self) -> None:
        self.temp.cleanup()

    def write_manifest(self, operator_extra: str = "") -> None:
        (self.framework / "framework-manifest.txt").write_text(
            "[framework]\ntools/**\n[operator]\ntools/_lib/credentials.json\n" + operator_extra
        )

    def run_check(self, *extra: str) -> subprocess.CompletedProcess[str]:
        return subprocess.run(
            [sys.executable, str(CHECKER), "--source-root", str(self.framework), "--installed-root", str(self.installed), *extra],
            text=True, capture_output=True, check=False,
            env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"},
        )

    def install_matching(self) -> None:
        for relative in ("git/guard.sh", "git/new-wrapper.sh", "_lib/manifest.sh"):
            shutil.copy2(self.source / relative, self.installed / relative)
        (self.installed / "_lib" / "credentials.json").write_text("different-operator-secret\n")

    def test_fails_loudly_and_classifies_stale_missing_and_installed_only(self) -> None:
        (self.installed / "git" / "guard.sh").write_text("broken\n")
        shutil.copy2(self.source / "_lib" / "manifest.sh", self.installed / "_lib" / "manifest.sh")
        (self.installed / "local-helper.sh").write_text("operator\n")
        result = self.run_check("--verbose")
        self.assertEqual(result.returncode, 1)
        self.assertIn("STALE git/guard.sh", result.stdout)
        self.assertIn("NOT_INSTALLED git/new-wrapper.sh", result.stdout)
        self.assertIn("INSTALLED_ONLY operator-or-unknown local-helper.sh", result.stdout)
        self.assertIn("FAIL deployed framework tools", result.stderr)

    def test_passes_only_when_every_manifest_owned_source_file_matches(self) -> None:
        self.install_matching()
        result = self.run_check()
        self.assertEqual(result.returncode, 0, result.stderr)
        self.assertIn("stale=0 not-installed=0 unsafe-alias=0", result.stdout)

    def test_exact_operator_directory_does_not_hide_framework_drift_beneath_it(self) -> None:
        self.install_matching()
        (self.installed / "git" / "guard.sh").write_text("drift-hidden-by-directory-entry\n")
        self.write_manifest("tools/git\n")

        result = self.run_check()

        self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
        self.assertIn("STALE git/guard.sh", result.stdout)

    def test_manifest_is_required_and_policy_changes_take_effect(self) -> None:
        self.install_matching()
        (self.installed / "git" / "guard.sh").write_text("operator-divergence\n")
        self.write_manifest("tools/git/guard.sh\n")
        self.assertEqual(self.run_check().returncode, 0)
        (self.framework / "framework-manifest.txt").unlink()
        result = self.run_check()
        self.assertEqual(result.returncode, 2)
        self.assertIn("CANNOT_ASSERT ownership manifest is missing", result.stderr)

    def test_empty_and_unreadable_source_census_cannot_assert(self) -> None:
        empty_framework = Path(self.temp.name) / "empty-framework"
        empty_source = empty_framework / "tools"
        empty_source.mkdir(parents=True)
        shutil.copy2(self.framework / "framework-manifest.txt", empty_framework / "framework-manifest.txt")
        # The canonical resolver is supplied outside the empty census solely so
        # this probe reaches the explicit minimum-population guard.
        result = subprocess.run([sys.executable, str(CHECKER), "--source-root", str(empty_framework), "--installed-root", str(self.installed)], text=True, capture_output=True)
        self.assertEqual(result.returncode, 2)
        self.assertIn("CANNOT_ASSERT", result.stderr)

        blocked = self.source / "blocked"
        blocked.mkdir(); (blocked / "hidden.sh").write_text("hidden\n"); blocked.chmod(0)
        try:
            result = self.run_check()
        finally:
            blocked.chmod(0o700)
        self.assertEqual(result.returncode, 2)
        self.assertIn("CANNOT_ASSERT", result.stderr)
        self.assertTrue("Permission denied" in result.stderr or "no readable/searchable mode" in result.stderr)

    def test_root_and_descendant_aliases_cannot_report_clean(self) -> None:
        result = subprocess.run([sys.executable, str(CHECKER), "--source-root", str(self.framework), "--installed-root", str(self.source)], text=True, capture_output=True)
        self.assertEqual(result.returncode, 2)
        self.assertIn("same filesystem object", result.stderr)

        shutil.copy2(self.source / "_lib" / "manifest.sh", self.installed / "_lib" / "manifest.sh")
        shutil.rmtree(self.installed / "git")
        (self.installed / "git").symlink_to(self.source / "git", target_is_directory=True)
        result = self.run_check()
        self.assertNotEqual(result.returncode, 0)
        self.assertTrue("symlinked directory" in result.stderr or "UNSAFE_ALIAS" in result.stdout)


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