"""Tests for canonical hash determinism with metadata exclusion."""

from __future__ import annotations

from abi_control_core.sdk.canonical_hash import (
    canonical_hash_excluding_metadata,
    canonical_hash_jcs_sha256,
)


class TestCanonicalHashDeterminism:
    """Test deterministic hashing with metadata field exclusion."""

    def test_identical_content_different_timestamps_same_hash(self) -> None:
        """
        Two evidence packs with different timestamps but identical content
        should hash identically.
        """
        manifest1 = {
            "schema_version": "evidence-pack-manifest/1.0",
            "run_id": "abc12345",
            "created_at": "2024-01-01T00:00:00Z",
            "canonical_fields": ["/components", "/hashes", "/summary/verdict"],
            "metadata_fields": ["/created_at", "/run_id"],
            "components": {
                "run_manifest": "manifest.json",
                "plan_graph": "plan_graph.json",
            },
            "hashes": {
                "manifest.json": "sha256:" + "a" * 64,
                "plan_graph.json": "sha256:" + "b" * 64,
            },
            "summary": {
                "total_files": 2,
                "total_artifacts": 0,
                "verdict": "pass",
            },
        }

        manifest2 = {
            "schema_version": "evidence-pack-manifest/1.0",
            "run_id": "def67890",
            "created_at": "2024-01-02T12:34:56Z",
            "canonical_fields": ["/components", "/hashes", "/summary/verdict"],
            "metadata_fields": ["/created_at", "/run_id"],
            "components": {
                "run_manifest": "manifest.json",
                "plan_graph": "plan_graph.json",
            },
            "hashes": {
                "manifest.json": "sha256:" + "a" * 64,
                "plan_graph.json": "sha256:" + "b" * 64,
            },
            "summary": {
                "total_files": 2,
                "total_artifacts": 0,
                "verdict": "pass",
            },
        }

        # Full hashes should differ (due to run_id and created_at)
        full_hash1 = canonical_hash_jcs_sha256(manifest1)
        full_hash2 = canonical_hash_jcs_sha256(manifest2)
        assert full_hash1 != full_hash2, "Full hashes should differ when metadata differs"

        # Canonical hashes (excluding metadata) should be identical
        metadata = [
            "/created_at",
            "/run_id",
            "/summary/total_files",
            "/summary/total_artifacts",
        ]
        canonical_hash1 = canonical_hash_excluding_metadata(manifest1, metadata)
        canonical_hash2 = canonical_hash_excluding_metadata(manifest2, metadata)
        assert canonical_hash1 == canonical_hash2, (
            "Canonical hashes should match when content is identical"
        )

    def test_different_content_different_hash(self) -> None:
        """
        Two evidence packs with different content should hash differently
        even when metadata is excluded.
        """
        manifest1 = {
            "schema_version": "evidence-pack-manifest/1.0",
            "run_id": "abc12345",
            "created_at": "2024-01-01T00:00:00Z",
            "canonical_fields": ["/components", "/hashes", "/summary/verdict"],
            "metadata_fields": ["/created_at", "/run_id"],
            "components": {
                "run_manifest": "manifest.json",
            },
            "hashes": {
                "manifest.json": "sha256:" + "a" * 64,
            },
            "summary": {
                "verdict": "pass",
            },
        }

        manifest2 = {
            "schema_version": "evidence-pack-manifest/1.0",
            "run_id": "abc12345",
            "created_at": "2024-01-01T00:00:00Z",
            "canonical_fields": ["/components", "/hashes", "/summary/verdict"],
            "metadata_fields": ["/created_at", "/run_id"],
            "components": {
                "run_manifest": "manifest.json",
            },
            "hashes": {
                # Different hash value
                "manifest.json": "sha256:" + "b" * 64,
            },
            "summary": {
                "verdict": "pass",
            },
        }

        metadata_fields = ["/created_at", "/run_id"]
        canonical_hash1 = canonical_hash_excluding_metadata(
            manifest1, metadata_fields
        )
        canonical_hash2 = canonical_hash_excluding_metadata(
            manifest2, metadata_fields
        )
        assert canonical_hash1 != canonical_hash2, (
            "Different content should produce different hashes"
        )

    def test_default_metadata_fields(self) -> None:
        """Test that default metadata fields are used when none are specified."""
        manifest = {
            "schema_version": "evidence-pack-manifest/1.0",
            "run_id": "abc12345",
            "created_at": "2024-01-01T00:00:00Z",
            "canonical_fields": ["/components", "/hashes"],
            "metadata_fields": ["/created_at", "/run_id"],
            "components": {"run_manifest": "manifest.json"},
            "hashes": {"manifest.json": "sha256:" + "a" * 64},
            "summary": {
                "total_files": 1,
                "total_artifacts": 0,
                "verdict": "pass",
            },
        }

        # Using default metadata fields
        hash_default = canonical_hash_excluding_metadata(manifest)

        # Explicitly specifying the same default fields
        hash_explicit = canonical_hash_excluding_metadata(
            manifest, ["/created_at", "/run_id", "/summary/total_files", "/summary/total_artifacts"]
        )

        assert hash_default == hash_explicit

    def test_nested_field_exclusion(self) -> None:
        """Test exclusion of nested fields via JSON Pointer."""
        obj1 = {
            "level1": {
                "level2": {
                    "metadata": "timestamp1",
                    "content": "data",
                }
            }
        }

        obj2 = {
            "level1": {
                "level2": {
                    "metadata": "timestamp2",
                    "content": "data",
                }
            }
        }

        # Exclude nested metadata field
        hash1 = canonical_hash_excluding_metadata(obj1, ["/level1/level2/metadata"])
        hash2 = canonical_hash_excluding_metadata(obj2, ["/level1/level2/metadata"])

        assert hash1 == hash2, "Hashes should match when nested metadata is excluded"

    def test_missing_field_graceful_handling(self) -> None:
        """Test that missing fields in JSON Pointer paths are handled gracefully."""
        manifest = {
            "schema_version": "evidence-pack-manifest/1.0",
            "components": {"run_manifest": "manifest.json"},
            "hashes": {"manifest.json": "sha256:" + "a" * 64},
        }

        # Try to exclude fields that don't exist
        hash_result = canonical_hash_excluding_metadata(
            manifest, ["/created_at", "/run_id", "/nonexistent/field"]
        )

        # Should not raise, and should produce a valid hash
        assert len(hash_result) == 64
        assert all(c in "0123456789abcdef" for c in hash_result)

    def test_verdict_changes_hash(self) -> None:
        """Changing verdict (a canonical field) should change the hash."""
        base_manifest = {
            "schema_version": "evidence-pack-manifest/1.0",
            "run_id": "abc12345",
            "created_at": "2024-01-01T00:00:00Z",
            "canonical_fields": ["/summary/verdict"],
            "metadata_fields": ["/created_at", "/run_id"],
            "components": {"run_manifest": "manifest.json"},
            "hashes": {"manifest.json": "sha256:" + "a" * 64},
            "summary": {"verdict": "pass"},
        }

        manifest_fail = {**base_manifest, "summary": {"verdict": "fail"}}

        hash_pass = canonical_hash_excluding_metadata(base_manifest, ["/created_at", "/run_id"])
        hash_fail = canonical_hash_excluding_metadata(manifest_fail, ["/created_at", "/run_id"])

        assert hash_pass != hash_fail, "Verdict change should produce different hashes"

    def test_run_manifest_timestamps_excluded(self) -> None:
        """Test that run_manifest timestamps can be excluded for deterministic comparison."""
        run1 = {
            "schema_version": "run-manifest/1.0",
            "run_id": "aabb0011",
            "gate": "intent-gate",
            "mode": "exploration",
            "trigger": "manual",
            "started_at": "2026-02-25T10:00:00Z",
            "completed_at": "2026-02-25T10:05:00Z",
            "intent_gate_hash": "sha256:" + "a" * 64,
            "verdict": "pass",
            "summary": {"total": 3, "passed": 3, "failed": 0, "skipped": 0, "blocker": 0},
        }

        run2 = {
            "schema_version": "run-manifest/1.0",
            "run_id": "ccdd2233",
            "gate": "intent-gate",
            "mode": "exploration",
            "trigger": "manual",
            "started_at": "2026-03-01T14:30:00Z",
            "completed_at": "2026-03-01T14:35:00Z",
            "intent_gate_hash": "sha256:" + "a" * 64,
            "verdict": "pass",
            "summary": {"total": 3, "passed": 3, "failed": 0, "skipped": 0, "blocker": 0},
        }

        # Exclude run_id and timestamps
        metadata_fields = ["/run_id", "/started_at", "/completed_at"]

        hash1 = canonical_hash_excluding_metadata(run1, metadata_fields)
        hash2 = canonical_hash_excluding_metadata(run2, metadata_fields)

        assert hash1 == hash2, (
            "Run manifests with same logic but different IDs/times "
            "should hash identically"
        )

    def test_span_timestamps_excluded(self) -> None:
        """Test that span timestamps can be excluded for deterministic comparison."""
        span1 = {
            "span_id": "ccdd0022",
            "name": "schema_validate",
            "start_time": "2026-02-25T10:00:00Z",
            "end_time": "2026-02-25T10:00:01Z",
            "status": "ok",
            "attributes": {"files_scanned": 5},
        }

        span2 = {
            "span_id": "eeff4455",
            "name": "schema_validate",
            "start_time": "2026-03-01T14:30:00Z",
            "end_time": "2026-03-01T14:30:01Z",
            "status": "ok",
            "attributes": {"files_scanned": 5},
        }

        # Exclude span_id and timestamps
        metadata_fields = ["/span_id", "/start_time", "/end_time"]

        hash1 = canonical_hash_excluding_metadata(span1, metadata_fields)
        hash2 = canonical_hash_excluding_metadata(span2, metadata_fields)

        assert hash1 == hash2, (
            "Spans with same logic but different IDs/times should hash identically"
        )

    def test_gate_receipt_deterministic_hash(self) -> None:
        """Test that gate receipts with different metadata but same decision hash identically."""
        receipt1 = {
            "schema_version": "gate-receipt/1.0",
            "receipt_id": "aabb0011",
            "gate_name": "intent-gate",
            "run_id": "ccdd2233",
            "task_id": "T-0201",
            "decision": "pass",
            "timestamp": "2026-02-25T10:05:00Z",
            "evidence_hash": "sha256:" + "e" * 64,
            "previous_receipt_hash": None,
            "verdict_details": {
                "checks_total": 5,
                "checks_passed": 5,
                "checks_failed": 0,
                "checks_skipped": 0,
                "blocker_count": 0,
            },
        }

        receipt2 = {
            "schema_version": "gate-receipt/1.0",
            "receipt_id": "eeff4455",
            "gate_name": "intent-gate",
            "run_id": "aabbccdd",
            "task_id": "T-0201",
            "decision": "pass",
            "timestamp": "2026-03-01T14:30:00Z",
            "evidence_hash": "sha256:" + "e" * 64,
            "previous_receipt_hash": None,
            "verdict_details": {
                "checks_total": 5,
                "checks_passed": 5,
                "checks_failed": 0,
                "checks_skipped": 0,
                "blocker_count": 0,
            },
        }

        # Full hashes should differ (due to receipt_id, run_id, timestamp)
        full_hash1 = canonical_hash_jcs_sha256(receipt1)
        full_hash2 = canonical_hash_jcs_sha256(receipt2)
        assert full_hash1 != full_hash2, "Full hashes should differ when metadata differs"

        # Canonical hashes (excluding metadata) should be identical
        metadata_fields = [
            "/receipt_id",
            "/run_id",
            "/timestamp",
            "/previous_receipt_hash",
        ]
        canonical_hash1 = canonical_hash_excluding_metadata(receipt1, metadata_fields)
        canonical_hash2 = canonical_hash_excluding_metadata(receipt2, metadata_fields)
        assert canonical_hash1 == canonical_hash2, (
            "Gate receipts with same decision but different metadata "
            "should hash identically"
        )

    def test_gate_receipt_different_decision_different_hash(self) -> None:
        """Test that gate receipts with different decisions hash differently."""
        receipt_pass = {
            "schema_version": "gate-receipt/1.0",
            "receipt_id": "aabb0011",
            "gate_name": "intent-gate",
            "run_id": "ccdd2233",
            "task_id": "T-0201",
            "decision": "pass",
            "timestamp": "2026-02-25T10:05:00Z",
            "evidence_hash": "sha256:" + "e" * 64,
            "previous_receipt_hash": None,
        }

        receipt_fail = {
            "schema_version": "gate-receipt/1.0",
            "receipt_id": "aabb0011",
            "gate_name": "intent-gate",
            "run_id": "ccdd2233",
            "task_id": "T-0201",
            "decision": "fail",
            "timestamp": "2026-02-25T10:05:00Z",
            "evidence_hash": "sha256:" + "e" * 64,
            "previous_receipt_hash": None,
        }

        # Even with metadata excluded, different decisions should produce different hashes
        metadata_fields = ["/receipt_id", "/run_id", "/timestamp", "/previous_receipt_hash"]
        hash_pass = canonical_hash_excluding_metadata(receipt_pass, metadata_fields)
        hash_fail = canonical_hash_excluding_metadata(receipt_fail, metadata_fields)
        assert hash_pass != hash_fail, "Different decisions should produce different hashes"

    def test_gate_receipt_different_evidence_hash_different_hash(self) -> None:
        """Test that gate receipts evaluating different evidence hash differently."""
        receipt1 = {
            "schema_version": "gate-receipt/1.0",
            "receipt_id": "aabb0011",
            "gate_name": "intent-gate",
            "run_id": "ccdd2233",
            "task_id": "T-0201",
            "decision": "pass",
            "timestamp": "2026-02-25T10:05:00Z",
            "evidence_hash": "sha256:" + "a" * 64,
            "previous_receipt_hash": None,
        }

        receipt2 = {
            "schema_version": "gate-receipt/1.0",
            "receipt_id": "aabb0011",
            "gate_name": "intent-gate",
            "run_id": "ccdd2233",
            "task_id": "T-0201",
            "decision": "pass",
            "timestamp": "2026-02-25T10:05:00Z",
            "evidence_hash": "sha256:" + "b" * 64,
            "previous_receipt_hash": None,
        }

        # Different evidence should produce different hashes even with metadata excluded
        metadata_fields = ["/receipt_id", "/run_id", "/timestamp", "/previous_receipt_hash"]
        hash1 = canonical_hash_excluding_metadata(receipt1, metadata_fields)
        hash2 = canonical_hash_excluding_metadata(receipt2, metadata_fields)
        assert hash1 != hash2, "Different evidence hashes should produce different canonical hashes"

    def test_gate_receipt_uses_schema_default_fields(self) -> None:
        """Test that gate receipts can use their schema-defined canonical/metadata fields."""
        receipt = {
            "schema_version": "gate-receipt/1.0",
            "receipt_id": "aabb0011",
            "gate_name": "intent-gate",
            "run_id": "ccdd2233",
            "task_id": "T-0201",
            "decision": "pass",
            "timestamp": "2026-02-25T10:05:00Z",
            "evidence_hash": "sha256:" + "e" * 64,
            "previous_receipt_hash": None,
            "canonical_fields": ["/decision", "/evidence_hash", "/gate_name", "/task_id"],
            "metadata_fields": ["/receipt_id", "/run_id", "/timestamp", "/previous_receipt_hash"],
        }

        # Use the schema-defined metadata fields
        hash_using_schema_fields = canonical_hash_excluding_metadata(
            receipt, receipt["metadata_fields"]
        )

        # Manually specify the same fields
        hash_manual = canonical_hash_excluding_metadata(
            receipt,
            ["/receipt_id", "/run_id", "/timestamp", "/previous_receipt_hash"],
        )

        assert hash_using_schema_fields == hash_manual, (
            "Using schema-defined fields should produce the same hash"
        )
