from __future__ import annotations

from datetime import datetime, timezone

import pytest

from icsd.core.errors import EvidenceSerialisationError
from icsd.core.evidence import (
    EVIDENCE_SCHEMA_VERSION,
    AuthoritySource,
    EvidenceBundle,
    EvidenceProvenance,
    EvidenceSignature,
    normalise_authority_sources,
    normalise_evidence_provenance,
    normalise_evidence_signatures,
    validate_evidence_payload,
)
from icsd.core.serialise import (
    canonical_json_bytes,
    canonical_json_bytes_for_signing,
    hash_canonical_json,
)


def test_canonical_json_bytes_are_deterministic_for_dictionary_key_order() -> None:
    left = {"b": 2, "a": [{"y": 2, "x": 1}]}
    right = {"a": [{"x": 1, "y": 2}], "b": 2}

    assert canonical_json_bytes(left) == canonical_json_bytes(right)
    assert canonical_json_bytes(left) == b'{"a":[{"x":1,"y":2}],"b":2}'


def test_hash_canonical_json_produces_expected_sha256_digest() -> None:
    value = {"a": 1, "b": [2, 3], "c": {"d": "x"}}

    assert (
        hash_canonical_json(value)
        == "188e54048bde2e27d6aab5e2a3848d8e02e8f00e220b94798b5e00a35dbb5cf4"
    )


def test_hash_canonical_json_wraps_json_type_error_as_evidence_serialisation_error() -> None:
    with pytest.raises(
        EvidenceSerialisationError,
        match="ICSD evidence data must be canonical-JSON-serialisable",
    ) as exc_info:
        hash_canonical_json(
            {
                "balance": 100,
                "captured_at": datetime(2026, 3, 3, 9, 30, tzinfo=timezone.utc),
            }
        )

    assert isinstance(exc_info.value.__cause__, TypeError)


def test_canonical_json_bytes_wraps_allow_nan_value_error_as_evidence_serialisation_error() -> None:
    with pytest.raises(
        EvidenceSerialisationError,
        match="ICSD evidence data must be canonical-JSON-serialisable",
    ) as exc_info:
        canonical_json_bytes({"balance": float("nan")})

    assert isinstance(exc_info.value.__cause__, ValueError)


def test_canonical_json_bytes_for_signing_excludes_selected_fields() -> None:
    payload = {
        "entity_id": "acct-001",
        "after_hash": "1" * 64,
        "before_hash": "0" * 64,
        "signatures": [{"algorithm": "ed25519"}],
    }

    signable = canonical_json_bytes_for_signing(
        payload,
        exclude_fields=("signatures",),
    )

    expected = (
        b'{"after_hash":"1111111111111111111111111111111111111111111111111111111111111111",'
        b'"before_hash":"0000000000000000000000000000000000000000000000000000000000000000",'
        b'"entity_id":"acct-001"}'
    )
    assert signable == expected


def test_evidence_bundle_from_states_builds_schema_v01_payload() -> None:
    before_state = {"balance": 100, "meta": {"currency": "GBP", "limit": 500}}
    after_state = {"meta": {"limit": 500, "currency": "GBP"}, "balance": 80}

    bundle = EvidenceBundle.from_states(
        transition_id="11111111-1111-4111-8111-111111111111",
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        authority="policy/account-update",
        before_state=before_state,
        after_state=after_state,
        timestamp=datetime(2026, 3, 3, 9, 30, tzinfo=timezone.utc),
    )

    assert bundle.schema_version == EVIDENCE_SCHEMA_VERSION
    assert bundle.timestamp == "2026-03-03T09:30:00Z"
    assert bundle.before_hash == hash_canonical_json(before_state)
    assert bundle.after_hash == hash_canonical_json(after_state)


def test_evidence_bundle_from_states_reports_before_state_serialisation_context() -> None:
    with pytest.raises(
        EvidenceSerialisationError,
        match="before_state must be canonical-JSON-serialisable",
    ) as exc_info:
        EvidenceBundle.from_states(
            transition_id="11111111-1111-4111-8111-111111111111",
            entity_type="account",
            entity_id="acct-001",
            actor="user:ops",
            authority="policy/account-update",
            before_state={
                "balance": 100,
                "captured_at": datetime(2026, 3, 3, 9, 30, tzinfo=timezone.utc),
            },
            after_state={"balance": 80},
        )

    assert isinstance(exc_info.value.__cause__, TypeError)


def test_authority_source_round_trip_mapping_conversion() -> None:
    source = AuthoritySource(
        source_type="registry",
        source_id="uk-companies-house",
        reference="https://find-and-update.company-information.service.gov.uk/company/00000006",
        details={"jurisdiction": "GB"},
    )

    assert AuthoritySource.from_dict(source.as_dict()) == source


def test_authority_source_deep_copies_nested_details_on_input_and_output() -> None:
    details = {"meta": {"jurisdiction": "GB"}, "tags": ["banking"]}
    source = AuthoritySource(
        source_type="registry",
        source_id="uk-companies-house",
        reference="company/00000006",
        details=details,
    )

    details["meta"]["jurisdiction"] = "US"
    details["tags"].append("retail")

    payload = source.as_dict()

    assert payload["details"] == {
        "meta": {"jurisdiction": "GB"},
        "tags": ["banking"],
    }

    payload["details"]["meta"]["jurisdiction"] = "DE"
    payload["details"]["tags"].append("changed")

    assert source.as_dict()["details"] == {
        "meta": {"jurisdiction": "GB"},
        "tags": ["banking"],
    }


def test_evidence_bundle_from_states_includes_authority_sources() -> None:
    bundle = EvidenceBundle.from_states(
        transition_id="11111111-1111-4111-8111-111111111111",
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        authority="policy/account-update",
        authority_sources=[
            {
                "source_type": "registry",
                "source_id": "uk-companies-house",
                "reference": "company/00000006",
                "details": {"jurisdiction": "GB"},
            },
            AuthoritySource(
                source_type="policy",
                source_id="hmrc-guidance",
                reference="hmrc-policy/v2026.03",
            ),
        ],
        before_state={"balance": 100},
        after_state={"balance": 80},
        timestamp="2026-03-03T09:30:00Z",
    )

    assert len(bundle.authority_sources) == 2
    assert bundle.authority_sources[0].source_id == "uk-companies-house"
    assert bundle.as_dict()["authority_sources"][1]["source_id"] == "hmrc-guidance"


def test_evidence_signature_deep_copies_nested_details_on_input() -> None:
    details = {"meta": {"signer": "ops-bot"}, "hints": ["primary"]}
    signature = EvidenceSignature(
        key_id="k-001",
        algorithm="ed25519",
        signature="deadbeef",
        details=details,
    )

    details["meta"]["signer"] = "tampered"
    details["hints"].append("secondary")

    assert signature.as_dict()["details"] == {
        "meta": {"signer": "ops-bot"},
        "hints": ["primary"],
    }


def test_evidence_provenance_deep_copies_nested_details_on_input() -> None:
    details = {"meta": {"channel": "append-only"}, "steps": ["import"]}
    provenance = EvidenceProvenance(
        source="audit-log",
        reference="log/entry/42",
        details=details,
    )

    details["meta"]["channel"] = "mutated"
    details["steps"].append("tampered")

    assert provenance.as_dict()["details"] == {
        "meta": {"channel": "append-only"},
        "steps": ["import"],
    }


def test_evidence_bundle_from_states_supports_signatures_and_provenance() -> None:
    def signature_hook(
        payload: dict[str, object],
        signable_bytes: bytes,
    ) -> list[dict[str, str]]:
        assert payload["entity_id"] == "acct-001"
        assert b'"entity_id":"acct-001"' in signable_bytes
        return [
            {
                "key_id": "k-001",
                "algorithm": "ed25519",
                "signature": "deadbeef",
            }
        ]

    def provenance_hook(payload: dict[str, object]) -> list[dict[str, str]]:
        assert payload["authority"] == "policy/account-update"
        return [
            {
                "source": "audit-log",
                "reference": "log/entry/42",
                "timestamp": "2026-03-03T09:45:00Z",
            }
        ]

    bundle = EvidenceBundle.from_states(
        transition_id="11111111-1111-4111-8111-111111111111",
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        authority="policy/account-update",
        before_state={"balance": 100},
        after_state={"balance": 80},
        signature_hook=signature_hook,
        provenance_hook=provenance_hook,
        signatures=[
            EvidenceSignature(
                key_id="k-000",
                algorithm="rsa-pss-sha256",
                signature="cafebabe",
            )
        ],
        provenance=[
            EvidenceProvenance(
                source="import",
                reference="migration/run/1",
            )
        ],
    )

    assert [item.key_id for item in bundle.signatures] == ["k-000", "k-001"]
    assert [item.source for item in bundle.provenance] == ["import", "audit-log"]


def test_evidence_bundle_from_states_snapshots_nested_metadata_inputs() -> None:
    authority_source = {
        "source_type": "registry",
        "source_id": "uk-companies-house",
        "reference": "company/00000006",
        "details": {"meta": {"jurisdiction": "GB"}, "tags": ["banking"]},
    }
    signature = {
        "key_id": "k-001",
        "algorithm": "ed25519",
        "signature": "deadbeef",
        "details": {"meta": {"signer": "ops-bot"}, "scopes": ["evidence"]},
    }
    provenance = {
        "source": "audit-log",
        "reference": "log/entry/42",
        "details": {"meta": {"channel": "append-only"}, "steps": ["import"]},
    }

    bundle = EvidenceBundle.from_states(
        transition_id="11111111-1111-4111-8111-111111111111",
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        authority="policy/account-update",
        authority_sources=[authority_source],
        before_state={"balance": 100},
        after_state={"balance": 80},
        signatures=[signature],
        provenance=[provenance],
    )

    authority_source["details"]["meta"]["jurisdiction"] = "US"
    authority_source["details"]["tags"].append("retail")
    signature["details"]["meta"]["signer"] = "tampered"
    signature["details"]["scopes"].append("mutated")
    provenance["details"]["meta"]["channel"] = "mutated"
    provenance["details"]["steps"].append("tampered")

    payload = bundle.as_dict()

    assert payload["authority_sources"][0]["details"] == {
        "meta": {"jurisdiction": "GB"},
        "tags": ["banking"],
    }
    assert payload["signatures"][0]["details"] == {
        "meta": {"signer": "ops-bot"},
        "scopes": ["evidence"],
    }
    assert payload["provenance"][0]["details"] == {
        "meta": {"channel": "append-only"},
        "steps": ["import"],
    }


def test_evidence_bundle_signable_bytes_excludes_signature_and_provenance_fields() -> None:
    bundle = EvidenceBundle.from_states(
        transition_id="11111111-1111-4111-8111-111111111111",
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        authority="policy/account-update",
        before_state={"balance": 100},
        after_state={"balance": 80},
        signatures=[
            {
                "key_id": "k-001",
                "algorithm": "ed25519",
                "signature": "deadbeef",
            }
        ],
        provenance=[{"source": "audit-log", "reference": "log/entry/42"}],
    )

    signable_text = bundle.signable_bytes().decode("utf-8")

    assert '"signatures"' not in signable_text
    assert '"provenance"' not in signable_text


def test_evidence_bundle_transparency_log_payload_strips_only_transparency_receipts() -> None:
    bundle = EvidenceBundle.from_states(
        transition_id="11111111-1111-4111-8111-111111111111",
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        authority="policy/account-update",
        before_state={"balance": 100},
        after_state={"balance": 80},
        provenance=[
            {
                "source": "policy-profile/retail-banking",
                "reference": "policy/account-update@2026.03",
            },
            {"source": "transparency-log/memory-log", "reference": "entry/42"},
            {"source": "import", "reference": "migration/run/1"},
        ],
    )

    payload = bundle.transparency_log_payload()

    assert [item["source"] for item in payload["provenance"]] == [
        "policy-profile/retail-banking",
        "import",
    ]
    assert bundle.as_dict()["provenance"][1]["source"] == "transparency-log/memory-log"


def test_evidence_bundle_transparency_log_payload_omits_empty_provenance() -> None:
    bundle = EvidenceBundle.from_states(
        transition_id="11111111-1111-4111-8111-111111111111",
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        authority="policy/account-update",
        before_state={"balance": 100},
        after_state={"balance": 80},
        provenance=[
            {"source": "transparency-log/memory-log", "reference": "entry/42"},
        ],
    )

    payload = bundle.transparency_log_payload()

    assert "provenance" not in payload


def test_evidence_bundle_round_trip_from_dict() -> None:
    bundle = EvidenceBundle.from_states(
        transition_id="11111111-1111-4111-8111-111111111111",
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        authority="policy/account-update",
        before_state={"balance": 100},
        after_state={"balance": 80},
        timestamp="2026-03-03T09:30:00+00:00",
    )

    reconstructed = EvidenceBundle.from_dict(bundle.as_dict())

    assert reconstructed == bundle


def test_evidence_bundle_integrity_verification_detects_mutation() -> None:
    before_state = {"balance": 100}
    after_state = {"balance": 80}
    bundle = EvidenceBundle.from_states(
        transition_id="11111111-1111-4111-8111-111111111111",
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        authority="policy/account-update",
        before_state=before_state,
        after_state=after_state,
        timestamp="2026-03-03T09:30:00Z",
    )

    assert bundle.verify_integrity(before_state=before_state, after_state=after_state)
    assert not bundle.verify_integrity(
        before_state=before_state,
        after_state={"balance": 85},
    )


def test_evidence_bundle_verify_integrity_reports_after_state_serialisation_context() -> None:
    bundle = EvidenceBundle.from_states(
        transition_id="11111111-1111-4111-8111-111111111111",
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        authority="policy/account-update",
        before_state={"balance": 100},
        after_state={"balance": 80},
        timestamp="2026-03-03T09:30:00Z",
    )

    with pytest.raises(
        EvidenceSerialisationError,
        match="after_state must be canonical-JSON-serialisable",
    ) as exc_info:
        bundle.verify_integrity(
            before_state={"balance": 100},
            after_state={
                "balance": 80,
                "captured_at": datetime(2026, 3, 3, 9, 31, tzinfo=timezone.utc),
            },
        )

    assert isinstance(exc_info.value.__cause__, TypeError)


def test_evidence_bundle_stream_event_reports_non_serialisable_metadata() -> None:
    bundle = EvidenceBundle.from_states(
        transition_id="11111111-1111-4111-8111-111111111111",
        entity_type="account",
        entity_id="acct-001",
        actor="user:ops",
        authority="policy/account-update",
        authority_sources=[
            AuthoritySource(
                source_type="registry",
                source_id="uk-companies-house",
                reference="company/00000006",
                details={
                    "captured_at": datetime(2026, 3, 3, 9, 30, tzinfo=timezone.utc),
                },
            )
        ],
        before_state={"balance": 100},
        after_state={"balance": 80},
    )

    with pytest.raises(
        EvidenceSerialisationError,
        match="ICSD evidence data must be canonical-JSON-serialisable",
    ) as exc_info:
        bundle.as_stream_event_payload(transition_name="debit_account")

    assert isinstance(exc_info.value.__cause__, TypeError)


def test_validate_evidence_payload_reports_missing_required_fields() -> None:
    errors = validate_evidence_payload({"schema_version": EVIDENCE_SCHEMA_VERSION})

    assert len(errors) == 1
    assert "Missing required evidence fields" in errors[0]
    assert "transition_id" in errors[0]


def test_evidence_schema_version_contract_remains_v01() -> None:
    assert EVIDENCE_SCHEMA_VERSION == "0.1"


def test_validate_evidence_payload_accepts_v01_required_fields_and_extension_fields() -> None:
    payload = {
        "schema_version": EVIDENCE_SCHEMA_VERSION,
        "transition_id": "11111111-1111-4111-8111-111111111111",
        "entity_type": "account",
        "entity_id": "acct-001",
        "actor": "user:ops",
        "authority": "policy/account-update",
        "timestamp": "2026-03-03T09:30:00Z",
        "before_hash": "0" * 64,
        "after_hash": "1" * 64,
        # Unknown extension keys should not break schema v0.1 compatibility checks.
        "future_extension": {"mode": "preview"},
    }

    assert validate_evidence_payload(payload) == ()


def test_validate_evidence_payload_reports_schema_and_integrity_errors() -> None:
    errors = validate_evidence_payload(
        {
            "schema_version": "9.9",
            "transition_id": "not-a-uuid",
            "entity_type": "",
            "entity_id": "acct-001",
            "actor": "",
            "authority": "policy/account-update",
            "timestamp": "2026-03-03T09:30:00+01:00",
            "before_hash": "bad-hash",
            "after_hash": "BAD-HASH",
        }
    )

    assert any("Unsupported evidence schema version" in error for error in errors)
    assert any("transition_id must be a valid UUID string" in error for error in errors)
    assert any("entity_type must be a non-empty string" in error for error in errors)
    assert any("actor must be a non-empty string" in error for error in errors)
    assert any("timestamp must include a UTC offset" in error for error in errors)
    assert any(
        "before_hash must be a lowercase hexadecimal sha256 digest" in error for error in errors
    )
    assert any(
        "after_hash must be a lowercase hexadecimal sha256 digest" in error for error in errors
    )


def test_validate_evidence_payload_reports_authority_source_errors() -> None:
    errors = validate_evidence_payload(
        {
            "schema_version": EVIDENCE_SCHEMA_VERSION,
            "transition_id": "11111111-1111-4111-8111-111111111111",
            "entity_type": "account",
            "entity_id": "acct-001",
            "actor": "user:ops",
            "authority": "policy/account-update",
            "authority_sources": [{"source_type": "registry", "reference": "company/00000006"}],
            "timestamp": "2026-03-03T09:30:00Z",
            "before_hash": "0" * 64,
            "after_hash": "1" * 64,
        }
    )

    assert any("authority_sources[0]" in error for error in errors)


def test_validate_evidence_payload_reports_signature_and_provenance_errors() -> None:
    errors = validate_evidence_payload(
        {
            "schema_version": EVIDENCE_SCHEMA_VERSION,
            "transition_id": "11111111-1111-4111-8111-111111111111",
            "entity_type": "account",
            "entity_id": "acct-001",
            "actor": "user:ops",
            "authority": "policy/account-update",
            "timestamp": "2026-03-03T09:30:00Z",
            "before_hash": "0" * 64,
            "after_hash": "1" * 64,
            "signatures": [{"algorithm": "ed25519", "signature": "deadbeef"}],
            "provenance": [{"reference": "log/entry/42"}],
        }
    )

    assert any("signatures[0]" in error for error in errors)
    assert any("provenance[0]" in error for error in errors)


def test_normalise_authority_sources_rejects_string_input() -> None:
    with pytest.raises(ValueError, match="authority_sources must be an iterable"):
        normalise_authority_sources("registry/uk")


def test_normalise_evidence_signatures_rejects_string_input() -> None:
    with pytest.raises(ValueError, match="signatures must be an iterable"):
        normalise_evidence_signatures("sig")


def test_normalise_evidence_provenance_rejects_string_input() -> None:
    with pytest.raises(ValueError, match="provenance must be an iterable"):
        normalise_evidence_provenance("prov")


def test_evidence_bundle_from_dict_raises_on_invalid_payload() -> None:
    with pytest.raises(ValueError, match="Unsupported evidence schema version"):
        EvidenceBundle.from_dict(
            {
                "schema_version": "0.2",
                "transition_id": "11111111-1111-4111-8111-111111111111",
                "entity_type": "account",
                "entity_id": "acct-001",
                "actor": "user:ops",
                "authority": "policy/account-update",
                "timestamp": "2026-03-03T09:30:00Z",
                "before_hash": "0" * 64,
                "after_hash": "1" * 64,
            }
        )
