import io
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from unittest.mock import patch

from release_bundle import FILES, package
from stack_check import StackCheck


class PipelineTest(unittest.TestCase):
    def setUp(self):
        parent = Path(__file__).resolve().parent.parent / "storage/app/private/ci-check"
        parent.mkdir(parents=True, exist_ok=True)
        self.temporary = tempfile.TemporaryDirectory(prefix="pipeline-test-", dir=parent)
        self.addCleanup(self.temporary.cleanup)
        self.root = Path(self.temporary.name)

    def test_container_commands_cannot_inherit_remote_docker_or_secret_environment(self):
        with patch.dict(os.environ, {"PATH": "/usr/bin", "DOCKER_HOST": "tcp://forbidden.invalid:2376", "APP_KEY": "fake"}, clear=True):
            checker = StackCheck(self.root, self.root)
        self.assertEqual("unix:///var/run/docker.sock", checker.environment["DOCKER_HOST"])
        self.assertNotIn("APP_KEY", checker.environment)
        with patch("stack_check.subprocess.run", return_value=subprocess.CompletedProcess([], 0, "{}")) as run:
            checker.compose("down", "--volumes")
        arguments = run.call_args.args[0]
        self.assertRegex(checker.project, r"^post76-ci-[0-9a-f]{32}$")
        self.assertIn(checker.project, arguments)
        self.assertEqual(str(self.root / "stack.env"), arguments[3])
        self.assertEqual(checker.environment, run.call_args.kwargs["env"])

    def test_failed_health_check_still_removes_only_its_synthetic_project(self):
        checker = StackCheck(self.root, self.root)
        calls = []
        model = {"services": {"nginx": {"ports": [8080]}, **{str(index): {} for index in range(8)}},
                 "networks": {"backend": {"internal": True}}}

        def compose(*arguments):
            calls.append(arguments)
            return subprocess.CompletedProcess([], 0, json.dumps(model))

        with patch.object(checker, "command"), patch.object(checker, "compose", side_effect=compose), \
                patch.object(checker, "request", side_effect=TimeoutError("synthetic timeout")), \
                patch("stack_check.socket.socket") as listener:
            listener.return_value.__enter__.return_value.getsockname.return_value = ("127.0.0.1", 18080)
            with self.assertRaises(TimeoutError):
                checker.run()
        self.assertEqual(("down", "--volumes", "--remove-orphans", "--timeout", "30"), calls[-1])
        summary = json.loads((self.root / "storage/app/private/ci-check/container-results/summary.json").read_text())
        self.assertEqual("failed", summary["result"])

    def test_release_rejects_untrusted_revision_before_docker(self):
        with patch("release_bundle.subprocess.run") as run:
            with self.assertRaises(ValueError):
                package(self.root, "main; arbitrary-command")
        run.assert_not_called()

    def test_lint_skips_private_operations_and_credentials(self):
        directory = self.root / "ci"
        directory.mkdir()
        source = Path(__file__).resolve().parent / "lint.py"
        (directory / "lint.py").write_text(source.read_text())
        (self.root / "vite.config.js").write_text("// Synthetic syntax fixture\n")
        operations = self.root / "ops"
        operations.mkdir()
        (operations / "collector.py").write_text("values = 1\n")
        for name in ["workspace", "credentials", ".credentials", "monitoring-secrets", ".post76-secrets-fixture"]:
            private = operations / name
            private.mkdir()
            (private / "synthetic.py").write_text("invalid source: private backup fixture must never be parsed")
        result = subprocess.run([sys.executable, str(directory / "lint.py")], env={"PATH": os.environ["PATH"]},
                                cwd=self.root, capture_output=True, text=True)
        self.assertEqual(0, result.returncode, result.stderr)
        self.assertIn("Python syntax checks passed", result.stdout)

    def test_release_rejects_missing_source_before_docker(self):
        with patch("release_bundle.subprocess.run") as run:
            with self.assertRaises(ValueError):
                package(self.root, "a" * 40)
        run.assert_not_called()

    def test_release_contains_only_allowlisted_sources_and_checksums(self):
        for name in FILES:
            path = self.root / name
            path.parent.mkdir(parents=True, exist_ok=True)
            path.write_text("synthetic public source")
        (self.root / ".env").write_text("SYNTHETIC_DO_NOT_COPY=1")
        private = self.root / "docker/secrets"
        private.mkdir()
        (private / "app_key").write_text("synthetic fixture only")
        process = unittest.mock.Mock(stdout=io.BytesIO(b"synthetic image archive"))
        process.wait.return_value = 0
        with patch("release_bundle.subprocess.run") as run, patch("release_bundle.subprocess.Popen", return_value=process):
            package(self.root, "a" * 40)
        destination = self.root / "storage/app/private/ci-check/release" / ("a" * 40)
        self.assertFalse((destination / ".env").exists())
        self.assertFalse((destination / "docker/secrets").exists())
        self.assertTrue((destination / "images.tar.gz").is_file())
        sums = (destination / "SHA256SUMS").read_text()
        self.assertIn("  images.tar.gz\n", sums)
        self.assertIn("  release.json\n", sums)
        self.assertEqual(len(FILES) + 2, len(sums.splitlines()))
        self.assertTrue(all(call.kwargs["env"]["DOCKER_HOST"] == "unix:///var/run/docker.sock" for call in run.call_args_list))


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