"""Temporary release checkouts must never issue an identity whose private key they cannot retain."""
from contextlib import ExitStack, redirect_stdout
from datetime import datetime, timedelta, timezone
import io
import json
from pathlib import Path
import tempfile
import unittest
from unittest import mock

from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography.x509.oid import NameOID

import cert_factory
import creds_store
import prepare_signing


def retained_identity(root, *, expires=90):
    key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, 'Synthetic signing test')])
    now = datetime.now(timezone.utc)
    cert = (x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(key.public_key())
            .serial_number(x509.random_serial_number()).not_valid_before(now - timedelta(days=1))
            .not_valid_after(now + timedelta(days=expires)).sign(key, hashes.SHA256()))
    p12 = pkcs12.serialize_key_and_certificates(b'test', key, cert, None,
                                               serialization.BestAvailableEncryption(b'synthetic'))
    (root / 'cert.p12').write_bytes(p12)
    (root / 'cert.meta.json').write_text(json.dumps({
        'cert_id': 'SYNTHETIC', 'not_after': (now + timedelta(days=expires)).isoformat(),
        'p12_password': 'synthetic'}))
    (root / 'profiles.manifest.json').write_text('{"profiles":[]}')
    return p12


def files(root):
    return {str(p.relative_to(root)): p.read_bytes() for p in root.rglob('*') if p.is_file()}


class RetainedSigningIdentityTests(unittest.TestCase):
    def setUp(self):
        self.stack = ExitStack()
        self.addCleanup(self.stack.close)
        self.root = Path(self.stack.enter_context(tempfile.TemporaryDirectory()))
        self.generate = self.stack.enter_context(mock.patch.object(cert_factory, 'generate_key_and_csr'))
        self.create = self.stack.enter_context(mock.patch.object(cert_factory, 'create_distribution_cert'))
        self.provider = self.stack.enter_context(mock.patch.object(creds_store, 'request'))
        self.output = self.stack.enter_context(redirect_stdout(io.StringIO()))

    def assert_refused_and_preserved(self):
        before = files(self.root)
        with self.assertRaisesRegex(SystemExit, 'retained Apple Distribution signing identity'):
            prepare_signing._load_signing_identity('synthetic-token', self.root)
        self.assertTrue(before == files(self.root))
        self.generate.assert_not_called()
        self.create.assert_not_called()
        annotation = next(line.split('::', 2)[2] for line in self.output.getvalue().splitlines()
                          if line.startswith('::error title=apple_signing_identity_required::'))
        self.assertFalse(json.loads(annotation)['certificate_creation_attempted'])

    def test_empty_checkout_refuses_without_provider_traffic(self):
        self.assert_refused_and_preserved()
        self.provider.assert_not_called()

    def test_corrupt_retained_material_is_preserved_without_provider_traffic(self):
        retained_identity(self.root)
        (self.root / 'cert.p12').write_bytes(b'synthetic-corrupt-p12')
        self.assert_refused_and_preserved()
        self.provider.assert_not_called()

    def test_near_expiry_material_is_preserved_for_reconciliation(self):
        retained_identity(self.root, expires=2)
        self.assert_refused_and_preserved()
        self.provider.assert_not_called()

    def test_revoked_retained_identity_only_reads_apple(self):
        retained_identity(self.root)
        self.provider.return_value = mock.Mock(status_code=404)
        self.assert_refused_and_preserved()
        self.provider.assert_called_once()
        self.assertEqual(self.provider.call_args.args[:2], ('GET', '/certificates/SYNTHETIC'))

    def test_indeterminate_apple_read_does_not_create_or_destroy_material(self):
        retained_identity(self.root)
        before = files(self.root)
        self.provider.side_effect = SystemExit('synthetic transport unavailable')
        with self.assertRaisesRegex(SystemExit, 'transport unavailable'):
            prepare_signing._load_signing_identity('synthetic-token', self.root)
        self.assertTrue(before == files(self.root))
        self.generate.assert_not_called()
        self.create.assert_not_called()

    def test_active_legacy_identity_reuses_identical_material_across_runs(self):
        p12 = retained_identity(self.root)
        before = files(self.root)
        self.provider.return_value = mock.Mock(status_code=200)
        for _ in range(2):
            identity = prepare_signing._load_signing_identity('synthetic-token', self.root)
            self.assertTrue(identity == ('SYNTHETIC', p12, 'synthetic', True))
        self.assertTrue(before == files(self.root))
        self.assertEqual(self.provider.call_count, 2)
        self.generate.assert_not_called()
        self.create.assert_not_called()


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