#!/usr/bin/env python3
"""Fail-closed behavior for centrally managed distribution identities."""
from __future__ import annotations

import base64
import hashlib
import json
import sys
import tempfile
import unittest
from datetime import datetime, timezone
from pathlib import Path
from unittest import mock

sys.path.insert(0, str(Path(__file__).resolve().parent))

import cert_factory  # noqa: E402
import creds_store  # noqa: E402
import prepare_signing  # noqa: E402


class RegistryManagedSigningTests(unittest.TestCase):
    def _managed_dir(self, state: str = "ready") -> tempfile.TemporaryDirectory:
        temp = tempfile.TemporaryDirectory()
        root = Path(temp.name)
        p12, metadata = b"p12", b"metadata"
        (root / "cert.p12").write_bytes(p12)
        (root / "cert.meta.json").write_bytes(metadata)
        marker = root / "cert.registry.json"
        marker.write_text(json.dumps({
            "schema_version": 1,
            "managed_by": "app-robot",
            "state": state,
            "p12_sha256": hashlib.sha256(p12).hexdigest(),
            "metadata_sha256": hashlib.sha256(metadata).hexdigest(),
        }))
        return temp

    def test_missing_managed_identity_never_enters_creation_path(self):
        with self._managed_dir() as raw:
            creds_dir = Path(raw)
            (creds_dir / "cert.p12").unlink()
            with mock.patch.object(cert_factory, "generate_key_and_csr") as generate:
                with mock.patch.object(cert_factory, "create_distribution_cert") as create:
                    with mock.patch.object(prepare_signing.creds_store, "invalidate_cache") as invalidate:
                        with self.assertRaisesRegex(SystemExit, "bundle is incomplete"):
                            prepare_signing._load_signing_identity("token", creds_dir)
        generate.assert_not_called()
        create.assert_not_called()
        invalidate.assert_not_called()

    def test_revoked_managed_identity_is_not_deleted_or_replaced(self):
        cached = creds_store.CachedCert(
            cert_id="CERT1",
            not_after=datetime(2027, 8, 1, tzinfo=timezone.utc),
            p12_bytes=b"p12",
            password="secret",
            certificate_sha256="a" * 64,
        )
        with self._managed_dir() as raw:
            with mock.patch.object(prepare_signing.creds_store, "load_cached_cert", return_value=cached):
                with mock.patch.object(prepare_signing.creds_store, "verify_cert_alive", return_value=False):
                    with mock.patch.object(cert_factory, "create_distribution_cert") as create:
                        with self.assertRaisesRegex(SystemExit, "not active on Apple or does not match"):
                            prepare_signing._load_signing_identity("token", Path(raw))
        create.assert_not_called()

    def test_active_managed_identity_is_reused(self):
        cached = creds_store.CachedCert(
            cert_id="CERT1",
            not_after=datetime(2027, 8, 1, tzinfo=timezone.utc),
            p12_bytes=b"p12",
            password="secret",
            certificate_sha256="a" * 64,
        )
        with self._managed_dir() as raw:
            with mock.patch.object(prepare_signing.creds_store, "load_cached_cert", return_value=cached):
                with mock.patch.object(prepare_signing.creds_store, "verify_cert_alive", return_value=True):
                    with mock.patch.object(cert_factory, "create_distribution_cert") as create:
                        result = prepare_signing._load_signing_identity("token", Path(raw))
        self.assertEqual(result, ("CERT1", b"p12", "secret", True))
        create.assert_not_called()

    def test_managed_liveness_binds_resource_id_to_certificate_bytes(self):
        remote_der = b"apple-certificate-der"
        response = mock.Mock(status_code=200)
        response.json.return_value = {"data": {"attributes": {
            "certificateContent": base64.b64encode(remote_der).decode(),
        }}}
        with mock.patch.object(creds_store, "request", return_value=response):
            digest = hashlib.sha256(remote_der).hexdigest()
            self.assertTrue(creds_store.verify_cert_alive("token", "CERT1", digest))
            self.assertFalse(creds_store.verify_cert_alive("token", "CERT1", "b" * 64))

    def test_updating_managed_bundle_fails_before_reading_identity(self):
        with self._managed_dir(state="updating") as raw:
            with mock.patch.object(prepare_signing.creds_store, "load_cached_cert") as load:
                with self.assertRaisesRegex(SystemExit, "not committed and ready"):
                    prepare_signing._load_signing_identity("token", Path(raw))
        load.assert_not_called()

    def test_ready_marker_rejects_tampered_bundle(self):
        with self._managed_dir() as raw:
            (Path(raw) / "cert.p12").write_bytes(b"tampered")
            with self.assertRaisesRegex(SystemExit, "digest mismatch"):
                prepare_signing._load_signing_identity("token", Path(raw))

    def test_metadata_without_ownership_marker_remains_legacy(self):
        with tempfile.TemporaryDirectory() as raw:
            meta = Path(raw) / "cert.meta.json"
            meta.write_text(json.dumps({"managed_by": "app-robot"}))
            self.assertFalse(creds_store.registry_managed(Path(raw)))


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