import copy
import importlib.util
import json
import tempfile
import unittest
from pathlib import Path
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat

ROOT = Path(__file__).resolve().parents[1]
REPO = ROOT.parents[1]
SCRIPT = ROOT / "scripts" / "validate_v3_candidate_manifest.py"
SPEC = importlib.util.spec_from_file_location("candidate_manifest", SCRIPT)
assert SPEC and SPEC.loader
MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE)


class CandidateManifestTest(unittest.TestCase):
    def setUp(self):
        self.value = json.loads((ROOT / "base_sepolia_v3_candidate_manifest.json").read_text())

    def _rehash(self, value):
        value["artifactHash"] = MODULE.content_hash(value)

    def _ready(self):
        value = copy.deepcopy(self.value)
        value["status"] = "READY_FOR_REHEARSAL"
        value["candidate"].update({
            "sourceCommit": "0" * 40,
            "currentContractsSourceSha256": "1" * 64,
            "sourceArchiveSha256": "2" * 64,
        })
        for index, contract in enumerate(value["contracts"].values(), start=1):
            contract.update({
                "address": f"0x{index:040x}",
                "creationCodeHash": "0x" + "11" * 32,
                "runtimeCodeHash": "0x" + "22" * 32,
                "constructorArguments": "0x",
            })
        value["roles"].update({"governanceSigners": ["0x0000000000000000000000000000000000000001"], "guardian": "g", "designPartner": "p"})
        self._rehash(value)
        return value

    def test_template_is_valid_but_explicitly_blocked_and_not_ready(self):
        self.assertEqual(MODULE.validate(self.value), [])
        self.assertNotEqual(self.value["status"], "READY_FOR_REHEARSAL")

    def test_mandatory_content_hash_rejects_tampering(self):
        self.value["enabledSurface"].append("tampered")
        errors = MODULE.validate(self.value)
        self.assertTrue(any("artifactHash mismatch" in error for error in errors))

    def test_production_cli_rejects_root_and_nested_duplicate_manifest_before_validation(self):
        import subprocess
        import sys
        for raw in (b'{"status":"a","status":"b"}', b'{"network":{"chainId":1,"chainId":2}}'):
            with tempfile.TemporaryDirectory() as directory:
                path = Path(directory) / "manifest.json"; path.write_bytes(raw)
                result = subprocess.run([sys.executable, str(SCRIPT), "--manifest", str(path)], text=True, capture_output=True, check=False)
                self.assertEqual(result.returncode, 2)
                self.assertIn("duplicate", result.stdout + result.stderr)

    def test_ready_manifest_requires_clean_reachable_commit_and_archive(self):
        errors = MODULE.validate(self._ready(), REPO, ROOT / "missing.tar")
        self.assertTrue(any("reachable" in error for error in errors))
        self.assertTrue(any("archive" in error for error in errors))

    def test_ready_manifest_rejects_unbound_source_hashes_before_artifact_claims(self):
        errors = MODULE.validate(self._ready())
        self.assertTrue(any("--repo-root" in error for error in errors))
        self.assertTrue(any("--source-archive" in error for error in errors))


    def test_shared_cross_language_canonicalization_corpus(self):
        corpus = json.loads((ROOT / "tests" / "canonicalization_corpus.json").read_text())
        for case in corpus:
            value = copy.deepcopy(self.value)
            if case["mutation"] == "non_bmp": value["bad😀"] = True
            elif case["mutation"] == "extra_candidate": value["candidate"]["unlisted"] = True; self._rehash(value)
            elif case["mutation"] == "missing_caps": del value["capsAtomic"]["global"]; self._rehash(value)
            elif case["mutation"] == "prohibited_drift": value["objectiveRuleSchema"]["prohibitedCategories"] = ["behavioral"]; self._rehash(value)
            elif case["mutation"] == "excluded_drift": value["excludedSurface"] = ["Solana"]; self._rehash(value)
            else: self.fail(case["name"])
            self.assertTrue(any(case["expected"] in error for error in MODULE.validate(value)), case["name"])

    def test_committed_source_reader_preserves_crlf_bytes(self):
        import subprocess
        import tempfile
        with tempfile.TemporaryDirectory() as directory:
            repo = Path(directory); source = repo / "v3-src/contracts/bonds/CrLf.sol"; source.parent.mkdir(parents=True)
            raw = b"pragma solidity 0.8.24;\r\ncontract CrLf {}\r\n"; source.write_bytes(raw)
            subprocess.run(["git", "init", "-q", str(repo)], check=True)
            subprocess.run(["git", "-C", str(repo), "config", "user.email", "test@example.invalid"], check=True)
            subprocess.run(["git", "-C", str(repo), "config", "user.name", "test"], check=True)
            subprocess.run(["git", "-C", str(repo), "add", "."], check=True); subprocess.run(["git", "-C", str(repo), "commit", "-qm", "crlf"], check=True)
            commit = subprocess.check_output(["git", "-C", str(repo), "rev-parse", "HEAD"], text=True).strip()
            entries, errors = MODULE.committed_source_entries(repo, commit)
            self.assertEqual(errors, []); self.assertEqual(entries["v3-src/contracts/bonds/CrLf.sol"], raw)

    def test_rejects_mainnet_and_objective_rule_schema_drift(self):
        self.value["network"]["chainId"] = 8453
        self.value["objectiveRuleSchema"]["allowedClasses"].append("QualityScore")
        self._rehash(self.value)
        errors = MODULE.validate(self.value)
        self.assertTrue(any("Base Sepolia" in error for error in errors))
        self.assertTrue(any("objective custody" in error for error in errors))

    def test_test_only_v2_attestation_fixture_has_exact_cross_language_bytes_and_rejects_field_tampering(self):
        fixture = REPO / "repos/vaultfire-sdk/tests/fixtures/base-sepolia-v3-attestation-v2.test.json"
        raw = fixture.read_bytes()
        receipt = MODULE.load_json_bytes(raw, "test v2 fixture", require_canonical_bytes=True)
        key = Ed25519PrivateKey.from_private_bytes(bytes(range(1, 33)))
        public = key.public_key()
        self.assertEqual(MODULE.verify_test_only_attestation_v2(receipt, public), [])
        generated = MODULE.create_test_only_attestation_v2(
            manifest_artifact_hash="11" * 32, artifact_evidence_bundle_sha256="22" * 32,
            validator_source_sha256="33" * 32, validator_transcript_sha256="44" * 32,
            deployment_verification_statement_sha256="55" * 32, key_id="test_only_v2_fixture",
            signing_key=key,
        )
        self.assertEqual(MODULE.canonical_json(generated), raw)
        for field in MODULE.ATTESTATION_V2_FIELDS:
            tampered = copy.deepcopy(receipt)
            tampered[field] = "x" if field != "chainId" else 1
            self.assertNotEqual(MODULE.verify_test_only_attestation_v2(tampered, public), [], field)
        with self.assertRaisesRegex(MODULE.AttestationUnavailable, "no receipt was emitted"):
            MODULE.ready_attestation({}, {})


class ArtifactEvidenceBundleTest(unittest.TestCase):
    def _solc(self):
        import os
        path = os.environ.get("VAULTFIRE_SOLC_0_8_24", "/home/user/.svm/0.8.24/solc-0.8.24")
        self.assertTrue(Path(path).is_file(), "exact local solc 0.8.24 is required for artifact evidence test")
        return path

    def _bundle_fixture(self):
        import io
        import os
        import subprocess
        import tarfile
        import tempfile
        from Crypto.Hash import keccak
        directory = tempfile.TemporaryDirectory()
        root = Path(directory.name)
        repo = root / "repo"; repo.mkdir()
        sources = {}
        for label, source_name in MODULE.SOURCE_NAMES.items():
            contents = f"// SPDX-License-Identifier: MIT\npragma solidity 0.8.24; contract {label} {{ constructor() {{}} }}\n"
            full = repo / "v3-src" / source_name; full.parent.mkdir(parents=True, exist_ok=True); full.write_text(contents)
            sources[source_name] = {"content": contents}
        subprocess.run(["git", "-C", str(repo), "init", "-q"], check=True)
        subprocess.run(["git", "-C", str(repo), "config", "user.email", "test@example.invalid"], check=True)
        subprocess.run(["git", "-C", str(repo), "config", "user.name", "test"], check=True)
        subprocess.run(["git", "-C", str(repo), "add", "v3-src/contracts"], check=True)
        subprocess.run(["git", "-C", str(repo), "commit", "-qm", "fixture"], check=True)
        commit = subprocess.check_output(["git", "-C", str(repo), "rev-parse", "HEAD"], text=True).strip()
        standard_input = {"language": "Solidity", "sources": sources, "settings": {"viaIR": True, "optimizer": {"enabled": True, "runs": 200}, "evmVersion": "paris", "metadata": {"bytecodeHash": "none"}, "outputSelection": {"*": {"*": ["abi", "metadata", "evm.bytecode.object", "evm.deployedBytecode.object"]}}}}
        input_path = root / "standard-input.json"; input_path.write_bytes(json.dumps(standard_input, separators=(",", ":")).encode())
        compiled = subprocess.run([self._solc(), "--standard-json"], input=input_path.read_bytes(), capture_output=True, check=True).stdout
        output_path = root / "standard-output.json"; output_path.write_bytes(compiled)
        output = json.loads(compiled)
        build_info = {"id": "synthetic-foundry-build-info", "language": "Solidity", "source_id_to_path": {str(index): source_name for index, source_name in enumerate(MODULE.SOURCE_NAMES.values())}}
        build_info_path = root / "build-info.json"; build_info_path.write_text(json.dumps(build_info))
        archive_path = root / "source.tar"
        entries = MODULE.committed_source_entries(repo, commit)[0]
        with tarfile.open(archive_path, "w", format=tarfile.USTAR_FORMAT) as archive:
            for path, data in sorted(entries.items()):
                member = tarfile.TarInfo(path); member.size = len(data); member.mode = 0o644; member.mtime = 0; member.uid = member.gid = 0
                archive.addfile(member, io.BytesIO(data))
        def kh(raw):
            d = keccak.new(digest_bits=256); d.update(raw); return "0x" + d.hexdigest()
        manifest = json.loads((ROOT / "base_sepolia_v3_candidate_manifest.json").read_text())
        manifest["status"] = MODULE.READY
        manifest["candidate"].update({"sourceCommit": commit, "currentContractsSourceSha256": MODULE.source_tree_hash(entries), "sourceArchiveSha256": __import__('hashlib').sha256(archive_path.read_bytes()).hexdigest()})
        bundle_contracts = {}
        for index, (label, source_name) in enumerate(MODULE.SOURCE_NAMES.items(), 1):
            contract = output["contracts"][source_name][label]
            creation = bytes.fromhex(contract["evm"]["bytecode"]["object"]); runtime = bytes.fromhex(contract["evm"]["deployedBytecode"]["object"])
            artifact = {"id": build_info["id"], "abi": contract["abi"], "bytecode": {"object": "0x" + creation.hex()}, "deployedBytecode": {"object": "0x" + runtime.hex()}, "metadata": json.loads(contract["metadata"])}
            artifact_path = root / f"{label}.json"; artifact_path.write_text(json.dumps(artifact))
            address = f"0x{index:040x}"; init_hash = kh(creation)
            manifest["contracts"][label] = {"address": address, "creationCodeHash": kh(creation), "runtimeCodeHash": kh(runtime), "constructorArguments": "0x"}
            bundle_contracts[label] = {"artifact": artifact_path.name, "sourceName": source_name, "contractName": label, "creationCodeHash": kh(creation), "runtimeCodeHash": kh(runtime), "constructorTypes": [], "constructorArguments": "0x", "initCodeHash": init_hash, "deploymentEvidence": {"address": address, "chainId": 84532, "transactionHash": "0x" + f"{index:064x}", "initCodeHash": init_hash, "runtimeCodeHash": kh(runtime)}}
        bundle = {"schema": MODULE.BUNDLE_SCHEMA, "bundleSha256": None, "sourceCommit": commit, "sourceTreeSha256": manifest["candidate"]["currentContractsSourceSha256"], "sourceArchiveSha256": manifest["candidate"]["sourceArchiveSha256"], "compiler": MODULE.COMPILER, "solcBinarySha256": MODULE.SOLC_SHA256, "settings": MODULE.SETTINGS, "standardJsonInput": input_path.name, "standardJsonOutput": output_path.name, "standardJsonInputSha256": __import__('hashlib').sha256(input_path.read_bytes()).hexdigest(), "standardJsonOutputSha256": __import__('hashlib').sha256(output_path.read_bytes()).hexdigest(), "buildInfo": build_info_path.name, "buildInfoSha256": __import__('hashlib').sha256(build_info_path.read_bytes()).hexdigest(), "submoduleGitlinks": {}, "deploymentVerificationStatement": "deployment-statement.json", "contracts": bundle_contracts}
        bundle["bundleSha256"] = MODULE.bundle_hash(bundle)
        bundle_path = root / "bundle.json"; bundle_path.write_text(json.dumps(bundle))
        manifest["candidate"]["artifactEvidenceBundleSha256"] = bundle["bundleSha256"]
        manifest["roles"] = {"governanceSigners": ["0x0000000000000000000000000000000000000001"], "guardian": "guardian", "designPartner": "partner", "treasury": None}
        manifest["artifactHash"] = MODULE.content_hash(manifest)
        statement = {"schema": "vaultfire.base-sepolia-v3-deployment-verification.v1", "chainId": 84532, "manifestArtifactHash": manifest["artifactHash"], "bundleSha256": bundle["bundleSha256"], "sourceTreeSha256": bundle["sourceTreeSha256"], "buildInfoSha256": bundle["buildInfoSha256"], "solcBinarySha256": MODULE.SOLC_SHA256, "validatorSourceSha256": "44" * 32, "validatorTranscriptSha256": "55" * 32, "contracts": {}, "signature": None, "publicKey": None}
        (root / "deployment-statement.json").write_bytes(MODULE.canonical_json(statement))
        return directory, manifest, repo, archive_path, bundle_path

    def test_synthetic_recompiled_bundle_can_validate_ready(self):
        directory, manifest, repo, archive, bundle = self._bundle_fixture()
        try:
            errors = MODULE.validate(manifest, repo, archive, bundle)
            self.assertTrue(any("production READY is disabled" in item for item in errors), errors)
            self.assertTrue(any("deployment verification contracts" in item for item in errors), errors)
        finally:
            directory.cleanup()

    def test_syntactically_valid_fake_hash_is_rejected_against_recompiled_artifact(self):
        directory, manifest, repo, archive, bundle = self._bundle_fixture()
        try:
            manifest["contracts"]["MultisigGovernanceV3"]["runtimeCodeHash"] = "0x" + "11" * 32
            manifest["artifactHash"] = MODULE.content_hash(manifest)
            errors = MODULE.validate(manifest, repo, archive, bundle)
            self.assertTrue(any("code hash" in error for error in errors))
        finally:
            directory.cleanup()

    def test_cli_writes_attestation_only_after_validated_ready_bundle(self):
        import subprocess
        import sys
        directory, manifest, repo, archive, bundle = self._bundle_fixture()
        try:
            manifest_path = Path(directory.name) / "manifest.json"; manifest_path.write_text(json.dumps(manifest))
            receipt_path = Path(directory.name) / "attestation.json"
            result = subprocess.run([sys.executable, str(SCRIPT), "--manifest", str(manifest_path), "--repo-root", str(repo), "--source-archive", str(archive), "--artifact-evidence-bundle", str(bundle), "--artifact-evidence-attestation-out", str(receipt_path)], text=True, capture_output=True, check=False)
            self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
            self.assertIn("production READY is disabled", result.stdout)
            self.assertFalse(receipt_path.exists())
        finally:
            directory.cleanup()

    def test_production_validator_duplicate_evidence_inputs_fail_before_digest_or_semantics(self):
        # Every raw artifact input reaches the shared duplicate-rejecting loader
        # before its hash/semantic comparison. Root and nested cases protect
        # against parser last-key-wins behavior at any object depth.
        targets = (
            ("artifact evidence bundle", "bundle.json"),
            ("standard-json input", "standard-input.json"),
            ("standard-json output", "standard-output.json"),
            ("Foundry build-info", "build-info.json"),
            ("artifact", "MultisigGovernanceV3.json"),
            ("deployment verification statement", "deployment-statement.json"),
        )
        for nesting, raw in (("root", b'{"a":1,"a":2}'), ("nested", b'{"a":{"b":1,"b":2}}')):
            for label, filename in targets:
                directory, manifest, repo, archive, bundle = self._bundle_fixture()
                try:
                    target = Path(directory.name) / filename
                    target.write_bytes(raw)
                    errors = MODULE.validate(manifest, repo, archive, bundle)
                    combined = " | ".join(errors)
                    self.assertIn("duplicate", combined, f"{label} {nesting}: {combined}")
                    self.assertNotIn("byte digest mismatch", combined, f"{label} {nesting}: duplicate must precede digest")
                finally:
                    directory.cleanup()


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


class SealedCompilerTest(unittest.TestCase):
    def test_verified_memfd_survives_path_replacement_and_rejects_symlink(self):
        import shutil
        from unittest import mock
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory); compiler = root / "solc"
            shutil.copyfile(MODULE.SOLC_PATH, compiler); compiler.chmod(0o700)
            with mock.patch.object(MODULE, "SOLC_PATH", compiler):
                sealed = MODULE._sealed_solc()
                self.assertIsNotNone(sealed)
                compiler.write_bytes(b"#!/bin/sh\necho attacker\n"); compiler.chmod(0o700)
                self.assertIn(b"0.8.24+commit.e11b9ed9", sealed.run(["--version"]).stdout)
                sealed.close()
            link = root / "link"; link.symlink_to(compiler)
            with mock.patch.object(MODULE, "SOLC_PATH", link):
                self.assertIsNone(MODULE._sealed_solc())
