from __future__ import annotations

import json
import hashlib
import os
import tempfile
import unittest
from pathlib import Path

from scripts import evaluate


ROOT = Path(__file__).resolve().parents[1]


class EvaluateTests(unittest.TestCase):
    def test_released_runtime_receipt_remains_frozen_during_migration(self) -> None:
        receipt = json.loads(
            (
                ROOT
                / "validation"
                / "candidate-v0.2.0"
                / "explorer-search-runtime-portable-collation-a3e0bdf7.json"
            ).read_text(encoding="utf-8")
        )
        released_tree = {
            "bytes": 31525576,
            "files": 148,
            "sha256": (
                "09ad960c7b44d0d1831cd8f4aa5a625fb2e7e4294a3ff2c6941bf1b1c127209c"
            ),
        }
        self.assertEqual(released_tree, receipt["bundle"]["tree"])
        self.assertEqual(
            {
                "algorithm": "sha256-sha256sum-lines-v1",
                "sha256": (
                    "50de3752fd48266aa59c5157324493d66195175140d6581846cabce93ce5a2cf"
                ),
                "files": 857,
                "bytes": 466605616,
            },
            evaluate.bundle_tree_identity(ROOT / "bundle"),
            "the exact v0.3.0 candidate tree must remain frozen",
        )

    def test_runtime_tree_identity_matches_explorer_bytewise_order(self) -> None:
        with tempfile.TemporaryDirectory(prefix=".test-tree-", dir=ROOT) as name:
            bundle = Path(name)
            payloads = {
                "a-lower.txt": b"lower",
                "_.txt": b"underscore",
                "B-dir/a-lower.txt": b"nested lower",
                "B-dir/A-upper.txt": b"nested upper",
                "A-upper.txt": b"upper",
                "0top.txt": b"zero",
                "-top.txt": b"dash",
                "!top.txt": b"bang",
            }
            for relative, payload in payloads.items():
                target = bundle / relative
                target.parent.mkdir(parents=True, exist_ok=True)
                target.write_bytes(payload)

            expected_order = [
                "!top.txt",
                "-top.txt",
                "0top.txt",
                "A-upper.txt",
                "B-dir/A-upper.txt",
                "B-dir/a-lower.txt",
                "_.txt",
                "a-lower.txt",
            ]
            self.assertEqual(
                expected_order,
                [
                    path.relative_to(bundle).as_posix()
                    for path in evaluate.explorer_ordered_bundle_files(bundle)
                ],
            )
            self.assertEqual(
                {
                    "algorithm": "sha256-sha256sum-lines-v1",
                    "sha256": (
                        "a5209fa8bbd7496d0905d703fc81f953ed7f67cd7ae927f5019f801e342c9bbb"
                    ),
                    "files": 8,
                    "bytes": 56,
                },
                evaluate.bundle_tree_identity(bundle),
            )

    def test_runtime_tree_identity_rejects_unsafe_tree_shapes(self) -> None:
        with tempfile.TemporaryDirectory(prefix=".test-tree-", dir=ROOT) as name:
            parent = Path(name)
            bundle = parent / "bundle"
            bundle.mkdir()
            bundle_link = parent / "bundle-link"
            bundle_link.symlink_to(bundle, target_is_directory=True)
            with self.assertRaisesRegex(ValueError, "real directory"):
                evaluate.bundle_tree_identity(bundle_link)

            fifo = bundle / "named-pipe"
            os.mkfifo(fifo)
            with self.assertRaisesRegex(ValueError, "non-regular"):
                evaluate.bundle_tree_identity(bundle)
            fifo.unlink()

            (bundle / "unsafe\\name.json").write_text("{}", encoding="utf-8")
            with self.assertRaisesRegex(ValueError, "unsafe Explorer"):
                evaluate.bundle_tree_identity(bundle)

        self.assertEqual(
            "😀" * 512,
            evaluate.explorer_safe_relative_path("😀" * 512),
        )
        with self.assertRaisesRegex(ValueError, "unsafe Explorer"):
            evaluate.explorer_safe_relative_path("😀" * 513)

    def test_candidate_locked_worker_calibration_covers_question_suite(self) -> None:
        questions_path = ROOT / "evaluation" / "questions.json"
        questions_bytes = questions_path.read_bytes()
        questions = json.loads(questions_bytes)
        runtime = json.loads(
            (ROOT / "evaluation" / "explorer-search-calibration-v0.3.0.json")
            .read_text(encoding="utf-8")
        )
        self.assertEqual(
            evaluate.locked_runtime_consumer(),
            runtime["required_consumer"],
        )
        required_consumer = runtime["required_consumer"]
        self.assertEqual("v0.6.2", required_consumer["release_tag"])
        self.assertEqual(368773937, required_consumer["immutable_release_id"])
        self.assertEqual(
            required_consumer["wrapper_sha256"],
            required_consumer["executable_materials"]["wrapper"]["sha256"],
        )
        self.assertEqual(
            required_consumer["invocation_lock_module_sha256"],
            required_consumer["executable_materials"]
            ["invocation_lock_module"]["sha256"],
        )
        self.assertEqual(6, len(required_consumer["executable_materials"]))
        self.assertEqual("0.3.0", runtime["expected_identity"]["version"])
        self.assertEqual(
            hashlib.sha256(questions_bytes).hexdigest(),
            runtime["calibration_suite_sha256"],
        )
        self.assertEqual(
            {question["id"] for question in questions["questions"]},
            {journey["calibration_question_id"] for journey in runtime["journeys"]},
        )
        self.assertEqual(26, len(runtime["journeys"]))
        question_by_id = {
            question["id"]: question for question in questions["questions"]
        }
        for journey in runtime["journeys"]:
            question = question_by_id[journey["calibration_question_id"]]
            assertions = journey["assertions"]
            expected_urls = {
                source["canonical_url"]
                for source in question["expected_sources"]
            }
            self.assertIn(journey["runtime_expected_source_url"], expected_urls)
            canonical_url = journey["runtime_expected_source_url"]
            self.assertEqual(
                {
                    "type": "wait_for_ranked_result",
                    "canonical_url": canonical_url,
                },
                journey["actions"][1],
            )
            self.assertEqual(
                {
                    "type": "click",
                    "selector": (
                        "[data-okf-ranked-results=\"primary\"] "
                        "[data-okf-ranked-result]"
                        "[data-result-canonical-url=\""
                        f"{canonical_url}\"]"
                    ),
                },
                journey["actions"][2],
            )
            self.assertTrue(
                any(
                    assertion.get("type") == "attribute"
                    and assertion.get("name") == "href"
                    and assertion.get("equals")
                    == journey["runtime_expected_source_url"]
                    for assertion in assertions
                )
            )
            asserted_caveats = {
                assertion.get("includes")
                for assertion in assertions
                if assertion.get("type") == "text"
            }
            self.assertLessEqual(set(journey["required_caveat_ids"]), asserted_caveats)
        for question in questions["questions"]:
            declared = {
                caveat_id
                for journey in runtime["journeys"]
                if journey["calibration_question_id"] == question["id"]
                for caveat_id in journey["required_caveat_ids"]
            }
            self.assertEqual(set(question["required_caveat_ids"]), declared)

    def test_canonical_removes_fragment_before_trailing_slash(self) -> None:
        self.assertEqual(
            "https://example.test/guide",
            evaluate.canonical("https://example.test/guide/#part"),
        )
        self.assertEqual(
            evaluate.canonical("https://example.test/guide/"),
            evaluate.canonical("https://example.test/guide#part"),
        )

    def test_compact_index_is_preferred_with_catalogue_fallback(self) -> None:
        with tempfile.TemporaryDirectory(prefix=".test-evaluate-", dir=ROOT) as name:
            bundle = Path(name)
            index_path = bundle / "data" / "search" / "index.json"
            index_path.parent.mkdir(parents=True)
            compact_record = {
                "id": "compact",
                "title": "Compact result",
                "url": "https://example.test/compact",
                "source_urls": ["https://example.test/compact"],
                "equivalent_urls": [],
                "heading_tokens": ["compact"],
                "body_tokens": ["compact", "result"],
            }
            index_path.write_text(
                json.dumps(
                    {
                        "schema": evaluate.SEARCH_INDEX_SCHEMA,
                        "snapshot_id": "compact-snapshot",
                        "record_count": 1,
                        "records": [compact_record],
                    }
                ),
                encoding="utf-8",
            )
            catalogue_path = bundle / "data" / "catalogue.json"
            catalogue_path.parent.mkdir(parents=True, exist_ok=True)
            catalogue_path.write_text(
                json.dumps(
                    {
                        "schema": evaluate.CATALOGUE_SCHEMA,
                        "snapshot_id": "catalogue-snapshot",
                        "record_count": 0,
                        "records": [],
                    }
                ),
                encoding="utf-8",
            )

            records, source = evaluate.load_records(bundle)
            self.assertEqual([compact_record], records)
            self.assertEqual("compact-search-index", source["kind"])

            index_path.unlink()
            records, source = evaluate.load_records(bundle)
            self.assertEqual([], records)
            self.assertEqual("catalogue-fallback", source["kind"])

    def test_question_success_and_expected_target_recall_are_distinct(self) -> None:
        contract = json.loads(evaluate.SEARCH_CONTRACT.read_text(encoding="utf-8"))
        records = [
            {
                "id": "a",
                "title": "Alpha",
                "url": "https://example.test/a/#fragment",
                "source_urls": ["https://example.test/b"],
                "equivalent_urls": ["https://example.test/a-alias"],
                "curation": "source-native",
                "heading_tokens": ["alpha"],
                "body_tokens": ["alpha"],
            },
            {
                "id": "c",
                "title": "Charlie",
                "url": "https://example.test/c",
                "curation": "source-native",
                "heading_tokens": ["charlie"],
                "body_tokens": ["charlie"],
            },
        ]
        questions = [
            {
                "id": "q1",
                "query": "alpha",
                "expected_sources": [
                    {"canonical_url": "https://example.test/a"},
                    {"canonical_url": "https://example.test/a-alias"},
                ],
            },
            {
                "id": "q2",
                "query": "missing",
                "expected_sources": [
                    {"canonical_url": "https://example.test/c"},
                ],
            },
        ]

        rows, metrics = evaluate.evaluate_questions(questions, records, contract, 1)

        self.assertTrue(rows[0]["expected_source_success_at_k"])
        self.assertEqual(1.0, rows[0]["expected_target_recall_at_k"])
        self.assertTrue(rows[0]["all_expected_targets_at_k"])
        self.assertFalse(rows[1]["expected_source_success_at_k"])
        self.assertEqual(0.5, metrics["expected_source_success_at_k"])
        self.assertAlmostEqual(2 / 3, metrics["expected_target_recall_at_k"])
        self.assertEqual(0.5, metrics["all_expected_targets_success_at_k"])

    def test_supporting_source_urls_do_not_satisfy_expected_targets(self) -> None:
        contract = json.loads(evaluate.SEARCH_CONTRACT.read_text(encoding="utf-8"))
        record = {
            "id": "source-separation",
            "title": "Safe route separation",
            "url": "https://example.test/primary",
            "source_urls": ["https://example.test/related-evidence"],
            "equivalent_urls": ["https://example.test/true-alias"],
            "curation": "reviewed",
            "heading_tokens": ["safe"],
            "body_tokens": [],
        }
        questions = [
            {
                "id": "related",
                "query": "safe",
                "expected_sources": [
                    {"canonical_url": "https://example.test/related-evidence"}
                ],
            },
            {
                "id": "alias",
                "query": "safe",
                "expected_sources": [
                    {"canonical_url": "https://example.test/true-alias"}
                ],
            },
        ]

        rows, _metrics = evaluate.evaluate_questions(
            questions, [record], contract, 10
        )

        self.assertFalse(rows[0]["expected_source_success_at_k"])
        self.assertTrue(rows[1]["expected_source_success_at_k"])

    def test_multi_term_search_rejects_single_common_term_noise(self) -> None:
        contract = json.loads(evaluate.SEARCH_CONTRACT.read_text(encoding="utf-8"))
        records = [
            {
                "id": "specific",
                "title": "Local land charges automation terms",
                "url": "https://example.test/specific",
                "curation": "reviewed",
                "heading_tokens": ["local", "land", "charges", "automation", "terms"],
                "body_tokens": [],
            },
            {
                "id": "noise",
                "title": "Unrelated land publication",
                "url": "https://example.test/noise",
                "curation": "source-native",
                "heading_tokens": ["land"],
                "body_tokens": [],
            },
        ]
        ranked = evaluate.rank("local land charges automation", records, contract)
        self.assertEqual(["specific"], [record["id"] for record in ranked])

    def test_minimum_should_match_uses_exact_integer_ceiling(self) -> None:
        contract = json.loads(evaluate.SEARCH_CONTRACT.read_text(encoding="utf-8"))
        self.assertEqual(
            {
                "apply_from_query_tokens": 3,
                "minimum_matches": 2,
                "ratio_numerator": 3,
                "ratio_denominator": 10,
            },
            contract["minimum_should_match"],
        )
        records = [
            {
                "id": "three-matches",
                "title": "Three matches",
                "url": "https://example.test/three",
                "curation": "source-native",
                "heading_tokens": ["one", "two", "three"],
                "body_tokens": [],
            },
            {
                "id": "two-matches",
                "title": "Two matches",
                "url": "https://example.test/two",
                "curation": "source-native",
                "heading_tokens": ["one", "two"],
                "body_tokens": [],
            },
        ]
        query = "one two three four five six seven eight nine ten eleven"
        ranked = evaluate.rank(query, records, contract)
        self.assertEqual([], ranked)

        records[0]["heading_tokens"].append("four")
        ranked = evaluate.rank(query, records, contract)
        self.assertEqual(["three-matches"], [record["id"] for record in ranked])

    def test_minimum_should_match_rejects_invalid_rational_denominator(self) -> None:
        contract = json.loads(evaluate.SEARCH_CONTRACT.read_text(encoding="utf-8"))
        contract["minimum_should_match"]["ratio_denominator"] = 0
        with self.assertRaisesRegex(ValueError, "positive integers"):
            evaluate.rank(
                "one two three",
                [
                    {
                        "id": "record",
                        "title": "Record",
                        "url": "https://example.test/record",
                        "curation": "source-native",
                        "heading_tokens": ["one", "two", "three"],
                        "body_tokens": [],
                    }
                ],
                contract,
            )

    def test_minimum_should_match_rejects_contract_drift(self) -> None:
        contract = json.loads(evaluate.SEARCH_CONTRACT.read_text(encoding="utf-8"))
        mutations = (
            (None, None, "exactly the four governed fields"),
            ("fallback", "or", "exactly the four governed fields"),
            ("apply_from_query_tokens", 99, "settled Explorer contract"),
            ("minimum_matches", True, "values must be integers"),
        )
        for field, value, error in mutations:
            with self.subTest(field=field, value=value):
                changed = json.loads(json.dumps(contract))
                if field is None:
                    del changed["minimum_should_match"]
                else:
                    changed["minimum_should_match"][field] = value
                with self.assertRaisesRegex(ValueError, error):
                    evaluate.rank("one two three", [], changed)

    def test_evaluator_tokenisation_matches_component_contract(self) -> None:
        contract = json.loads(evaluate.SEARCH_CONTRACT.read_text(encoding="utf-8"))
        self.assertEqual(
            {"cafe", "data", "paid", "price", "records"},
            evaluate.tokens("Café price-paid data-and records x", contract),
        )
        self.assertEqual(set(), evaluate.tokens("a\u1ab0b", contract))
        for field, value, error in (
            ("token_pattern", "[a-z]+", "token_pattern"),
            ("token_min_length", 1, "token_min_length"),
            ("stopwords", ["the", "and"], "stopwords"),
        ):
            with self.subTest(field=field):
                changed = json.loads(json.dumps(contract))
                changed[field] = value
                with self.assertRaisesRegex(ValueError, error):
                    evaluate.tokens("price-paid", changed)

    def test_forbidden_targets_are_executable_negative_assertions(self) -> None:
        contract = json.loads(evaluate.SEARCH_CONTRACT.read_text(encoding="utf-8"))
        records = [
            {
                "id": "wrong-jurisdiction",
                "title": "Property register",
                "url": "https://www.ros.gov.uk/",
                "curation": "source-native",
                "heading_tokens": ["property", "register"],
                "body_tokens": [],
            }
        ]
        questions = [
            {
                "id": "q-negative",
                "query": "property register",
                "expected_sources": [],
                "required_caveat_ids": ["CAV-BOUNDED-COVERAGE"],
                "must_not_retrieve": [
                    {
                        "target_id": "NEG-CROSS-JURISDICTION",
                        "canonical_url": "https://www.ros.gov.uk/",
                        "max_rank": 5,
                    }
                ],
            }
        ]
        rows, metrics = evaluate.evaluate_questions(questions, records, contract, 5)
        self.assertFalse(rows[0]["must_not_retrieve_passed"])
        self.assertEqual(1, rows[0]["must_not_retrieve_hits"][0]["rank"])
        self.assertEqual(1, metrics["must_not_retrieve_hit_count"])
        self.assertEqual(0.0, metrics["must_not_retrieve_pass_rate"])

    def test_question_contract_requires_caveats_and_forbidden_targets(self) -> None:
        payload = {
            "suite_partition": "calibration",
            "caveat_registry": [{"id": "CAV-ONE", "text": "One"}],
            "questions": [
                {
                    "id": "Q1",
                    "expected_sources": [
                        {"canonical_url": "https://example.test/right"}
                    ],
                    "runtime_expected_source_url": "https://example.test/right",
                    "required_caveat_ids": ["CAV-ONE"],
                    "must_not_retrieve": [
                        {
                            "target_id": "NEG-ONE",
                            "canonical_url": "https://example.test/wrong",
                            "max_rank": 5,
                            "reason": "A bounded executable near miss.",
                        }
                    ],
                }
            ],
        }
        evaluate.validate_question_contract(payload)
        payload["questions"][0]["must_not_retrieve"] = []
        with self.assertRaisesRegex(ValueError, "forbidden targets"):
            evaluate.validate_question_contract(payload)

    def test_acceptance_review_requires_exact_independent_coverage(self) -> None:
        questions = [
            {
                "id": "Q1",
                "hard_failure_ids": ["HF-AUTHORITY", "HF-COVERAGE"],
                "required_caveat_ids": ["CAV-AUTHORITY"],
            }
        ]
        review = {
            "schema": evaluate.ACCEPTANCE_REVIEW_SCHEMA,
            "status": "pass",
            "suite_sha256": "a" * 64,
            "bundle_release_root_sha256": "b" * 64,
            "reviewer": {
                "role": "independent evaluation reviewer",
                "kind": "AI-assisted independent process",
                "reviewed_at": "2026-07-29",
                "independent_of_retrieval_implementation": True,
            },
            "question_reviews": [
                {
                    "question_id": "Q1",
                    "source_resolution": True,
                    "expected_propositions_verified": True,
                    "near_miss_rule_verified": True,
                    "caveat_coverage": True,
                    "hard_failure_ids_reviewed": [
                        "HF-AUTHORITY",
                        "HF-COVERAGE",
                    ],
                    "required_caveat_ids_verified": ["CAV-AUTHORITY"],
                    "hard_failures_observed": [],
                }
            ],
            "held_out_adversarial": [
                {
                    "id": f"ADV-{index}",
                    "status": "pass",
                    "new_critical_category": False,
                    "precision_acceptable": True,
                    "safety_behavior_verified": True,
                }
                for index in range(1, 7)
            ],
        }

        result = evaluate.validate_acceptance_review(
            review, questions, "a" * 64, "b" * 64
        )
        self.assertEqual(0, result["hard_failure_count"])
        self.assertEqual(
            1.0, result["independent_review_caveat_coverage"]
        )
        self.assertEqual(
            1.0, result["independent_review_source_resolution_coverage"]
        )

        review["question_reviews"][0]["hard_failures_observed"] = ["HF-COVERAGE"]
        with self.assertRaisesRegex(ValueError, "hard failure was observed"):
            evaluate.validate_acceptance_review(
                review, questions, "a" * 64, "b" * 64
            )

    def test_forbidden_targets_must_exist_in_candidate_and_fit_k(self) -> None:
        questions = [
            {
                "id": "Q1",
                "expected_sources": [
                    {"canonical_url": "https://example.test/right"}
                ],
                "must_not_retrieve": [
                    {
                        "target_id": "NEG-ONE",
                        "canonical_url": "https://example.test/wrong",
                        "max_rank": 5,
                    }
                ],
            }
        ]
        records = [
            {
                "url": "https://example.test/right",
                "equivalent_urls": [],
            }
        ]
        with self.assertRaisesRegex(ValueError, "absent from the candidate"):
            evaluate.validate_forbidden_targets(questions, records, 10)
        records.append(
            {
                "url": "https://example.test/wrong",
                "equivalent_urls": [],
            }
        )
        evaluate.validate_forbidden_targets(questions, records, 10)
        with self.assertRaisesRegex(ValueError, "cannot execute"):
            evaluate.validate_forbidden_targets(questions, records, 4)

    def test_runtime_evidence_measures_source_and_caveat_assertions(self) -> None:
        questions = [
            {
                "id": "Q1",
                "runtime_expected_source_url": "https://example.test/right",
                "expected_sources": [
                    {"canonical_url": "https://example.test/right"}
                ],
                "required_caveat_ids": ["CAV-ONE"],
            }
        ]
        with tempfile.TemporaryDirectory(prefix=".test-runtime-", dir=ROOT) as name:
            root = Path(name)
            bundle = root / "bundle"
            bundle.mkdir()
            expected_identity = {
                "schema": "okf-explorer-large-corpus.v1",
                "id": "https://example.test/okf-explorer.json",
                "version": "0.3.0",
                "snapshot": "example-snapshot",
            }
            (bundle / "okf-explorer.json").write_text(
                json.dumps(
                    {
                        "schema": expected_identity["schema"],
                        "@id": expected_identity["id"],
                        "version": expected_identity["version"],
                        "snapshot": expected_identity["snapshot"],
                    },
                    sort_keys=True,
                )
                + "\n",
                encoding="utf-8",
            )
            manifest = {
                "schema": evaluate.RUNTIME_JOURNEY_SCHEMA,
                "bundle_descriptor": "okf-explorer.json",
                "calibration_suite_sha256": "a" * 64,
                "required_consumer": evaluate.locked_runtime_consumer(),
                "expected_identity": expected_identity,
                "journeys": [
                    {
                        "id": "q1",
                        "calibration_question_id": "Q1",
                        "runtime_expected_source_url": "https://example.test/right",
                        "required_caveat_ids": ["CAV-ONE"],
                        "assertions": [
                            {
                                "type": "attribute",
                                "name": "href",
                                "equals": "https://example.test/right",
                            },
                            {"type": "text", "includes": "CAV-ONE"},
                        ],
                    }
                ],
            }
            manifest_path = root / "journeys.json"
            manifest_path.write_text(
                json.dumps(manifest, sort_keys=True) + "\n", encoding="utf-8"
            )
            receipt = {
                "schema": evaluate.RUNTIME_RECEIPT_SCHEMA,
                "status": "passed",
                "consumer": {
                    field: value
                    for field, value in evaluate.locked_runtime_consumer().items()
                    if field
                    not in {
                        "consumer_lock",
                        "release_tag",
                        "annotated_tag_object_sha",
                        "immutable_release_id",
                    }
                },
                "journey_manifest": {
                    "sha256": hashlib.sha256(manifest_path.read_bytes()).hexdigest()
                },
                "bundle": {
                    "tree": evaluate.bundle_tree_identity(bundle),
                    "identity": manifest["expected_identity"],
                    "expected_identity": manifest["expected_identity"],
                },
                "journeys": [
                    {"id": "q1", "terminal": {"status": "passed"}}
                ],
            }
            receipt_path = root / "receipt.json"
            receipt_path.write_text(
                json.dumps(receipt, sort_keys=True) + "\n", encoding="utf-8"
            )
            result = evaluate.validate_runtime_evidence(
                manifest_path,
                receipt_path,
                questions,
                "a" * 64,
                bundle,
            )
            receipt["consumer"]["source_commit"] = "0" * 40
            receipt_path.write_text(
                json.dumps(receipt, sort_keys=True) + "\n", encoding="utf-8"
            )
            with self.assertRaisesRegex(ValueError, "required Explorer consumer"):
                evaluate.validate_runtime_evidence(
                    manifest_path,
                    receipt_path,
                    questions,
                    "a" * 64,
                    bundle,
                )
            locked = evaluate.locked_runtime_consumer()
            receipt["consumer"]["source_commit"] = locked["source_commit"]
            receipt["consumer"]["wrapper_sha256"] = "0" * 64
            receipt_path.write_text(
                json.dumps(receipt, sort_keys=True) + "\n", encoding="utf-8"
            )
            with self.assertRaisesRegex(ValueError, "required Explorer consumer"):
                evaluate.validate_runtime_evidence(
                    manifest_path,
                    receipt_path,
                    questions,
                    "a" * 64,
                    bundle,
                )
            receipt["consumer"]["wrapper_sha256"] = locked[
                "wrapper_sha256"
            ]
            receipt["consumer"]["executable_materials"]["contract_module"][
                "bytes"
            ] -= 1
            receipt_path.write_text(
                json.dumps(receipt, sort_keys=True) + "\n", encoding="utf-8"
            )
            with self.assertRaisesRegex(ValueError, "required Explorer consumer"):
                evaluate.validate_runtime_evidence(
                    manifest_path,
                    receipt_path,
                    questions,
                    "a" * 64,
                    bundle,
                )
        self.assertEqual(1.0, result["source_resolution_coverage"])
        self.assertEqual(1.0, result["required_caveat_assertion_coverage"])


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