mod support;

use levcs_protocol::v2::*;
use levcs_protocol::CanonicalCodec;
use support::*;

fn destination_event_for(evidence: &TransactionEvidenceV1) -> CommittedTransactionV1 {
    let repo_id = id(3);
    // The actor is read off the evidence rather than recomputed here. A helper
    // that derived it a second way could agree with `validate_mirror_application`
    // by coincidence while every production sequencer disagreed, which is the
    // defect this accessor exists to remove.
    let actor = evidence.actor();
    let operation_id = match evidence {
        TransactionEvidenceV1::MirrorEventV1 {
            source_instance,
            source_event,
            ..
        } => mirror_event_operation_id(
            *source_instance,
            repo_id,
            source_event.transaction.repo_sequence,
        ),
        TransactionEvidenceV1::MirrorSnapshotV1 {
            source_instance,
            source_snapshot,
            destination_projection,
            projected_manifest_digest,
            ..
        } => mirror_snapshot_operation_id(
            *source_instance,
            repo_id,
            source_snapshot.snapshot.generation_digest().unwrap(),
            *destination_projection,
            *projected_manifest_digest,
        ),
        // Named individually: these four are not mirror applications, so they
        // have no derived operation ID. A catch-all here would silently give a
        // newly added mirror variant a constant operation ID and a passing
        // test.
        TransactionEvidenceV1::ClientV2 { .. }
        | TransactionEvidenceV1::LegacyMigrationV1 { .. }
        | TransactionEvidenceV1::ProjectionAdminV1 { .. }
        | TransactionEvidenceV1::AdministrativeV1 { .. } => [0x71; 16],
    };
    CommittedTransactionV1 {
        repo_id,
        repo_sequence: 1,
        previous_event_digest: id(0),
        sequenced_at_micros: 20_000,
        source_kind: evidence.source_kind(),
        actor,
        operation_id,
        operation_digest: id(72),
        signed_evidence_digest: evidence.evidence_digest().unwrap(),
        old_authority: id(4),
        new_authority: id(4),
        refs: vec![AppliedRefV1 {
            target: RefTarget::Branch("main".into()),
            old: None,
            new: Some(id(7)),
            force: false,
        }],
        object_ids: vec![id(31)],
        commit_ids: vec![id(31)],
        resulting_state_digest: id(73),
    }
}

#[test]
fn mirror_snapshot_consumer_accepts_both_inline_and_staged_atomic_publication() {
    let evidence = all_evidence()
        .into_iter()
        .find(|value| matches!(value, TransactionEvidenceV1::MirrorSnapshotV1 { .. }))
        .unwrap();
    let event = destination_event_for(&evidence);
    validate_mirror_application(
        MirrorApplicationKindV1::SnapshotInline,
        &evidence,
        &event,
        None,
    )
    .unwrap();

    let (
        destination_projection,
        projected_manifest_digest,
        projected_object_count,
        projected_object_bytes,
    ) = match &evidence {
        TransactionEvidenceV1::MirrorSnapshotV1 {
            destination_projection,
            projected_manifest_digest,
            projected_object_count,
            projected_object_bytes,
            ..
        } => (
            *destination_projection,
            *projected_manifest_digest,
            *projected_object_count,
            *projected_object_bytes,
        ),
        _ => unreachable!(),
    };
    let install = StagedProjectionInstallV1 {
        session_id: [0x72; 16],
        manifest_digest: projected_manifest_digest,
        projection: destination_projection,
        object_count: projected_object_count,
        object_bytes: projected_object_bytes,
        membership_root: id(74),
        artifact_set_digest: id(75),
    };
    validate_mirror_application(
        MirrorApplicationKindV1::SnapshotStaged,
        &evidence,
        &event,
        Some(&install),
    )
    .unwrap();

    let mut wrong = install;
    wrong.object_count += 1;
    assert!(validate_mirror_application(
        MirrorApplicationKindV1::SnapshotStaged,
        &evidence,
        &event,
        Some(&wrong)
    )
    .is_err());
}

#[test]
fn mirror_event_consumer_distinguishes_projected_from_cursor_only() {
    let evidence = all_evidence()
        .into_iter()
        .find(|value| matches!(value, TransactionEvidenceV1::MirrorEventV1 { .. }))
        .unwrap();
    let projected = destination_event_for(&evidence);
    validate_mirror_application(
        MirrorApplicationKindV1::ProjectedEvent,
        &evidence,
        &projected,
        None,
    )
    .unwrap();

    let cursor_only = CommittedTransactionV1 {
        refs: vec![],
        object_ids: vec![],
        commit_ids: vec![],
        resulting_state_digest: id(4),
        ..destination_event_for(&evidence)
    };
    validate_mirror_application(
        MirrorApplicationKindV1::CursorOnlyEvent,
        &evidence,
        &cursor_only,
        None,
    )
    .unwrap();
    assert!(validate_mirror_application(
        MirrorApplicationKindV1::ProjectedEvent,
        &evidence,
        &cursor_only,
        None
    )
    .is_err());
}

#[test]
fn evidence_actor_is_the_binding_a_destination_event_must_restate() {
    // One expected principal per `all_evidence()` entry, in order: the two
    // client pushes are signed by keys 1 and 2, both mirror variants name
    // instance key 7, and the three administrative variants are signed by the
    // admin key 8. Written out rather than derived, so a variant that starts
    // reporting a different principal fails here instead of agreeing with a
    // second copy of the same mistake.
    let expected = [
        key(1).public().0,
        key(2).public().0,
        key(7).public().0,
        key(7).public().0,
        key(8).public().0,
        key(8).public().0,
        key(8).public().0,
    ];
    let evidence = all_evidence();
    assert_eq!(evidence.len(), expected.len());
    for (value, expected_actor) in evidence.iter().zip(expected) {
        assert_eq!(value.actor(), expected_actor);
    }

    // The failure this accessor exists to prevent. Filling `actor` from the
    // destination instance's own signing key produces an event that signs,
    // fences, and becomes durable, and only then fails mirror verification —
    // at which point no caller can withdraw it.
    let snapshot_evidence = evidence
        .iter()
        .find(|value| matches!(value, TransactionEvidenceV1::MirrorSnapshotV1 { .. }))
        .unwrap();
    let event = destination_event_for(snapshot_evidence);
    validate_mirror_application(
        MirrorApplicationKindV1::SnapshotInline,
        snapshot_evidence,
        &event,
        None,
    )
    .unwrap();
    let destination_signed_actor = CommittedTransactionV1 {
        actor: key(9).public().0,
        ..event
    };
    assert!(validate_mirror_application(
        MirrorApplicationKindV1::SnapshotInline,
        snapshot_evidence,
        &destination_signed_actor,
        None
    )
    .is_err());
}

#[test]
fn consumer_dispatch_is_total_for_every_transaction_source_kind() {
    let mut seen = [false; 6];
    for evidence in all_evidence() {
        let index = match evidence.source_kind() {
            SourceKindV1::Client => 0,
            SourceKindV1::MirrorSnapshot => 1,
            SourceKindV1::MirrorEvent => 2,
            SourceKindV1::LegacyMigration => 3,
            SourceKindV1::ProjectionAdmin => 4,
            SourceKindV1::Administrative => 5,
        };
        seen[index] = true;
    }
    assert!(seen.into_iter().all(|value| value));
    assert_eq!(SourceKindV1::Client as u8, 1);
    assert_eq!(SourceKindV1::MirrorSnapshot as u8, 2);
    assert_eq!(SourceKindV1::MirrorEvent as u8, 3);
    assert_eq!(SourceKindV1::LegacyMigration as u8, 4);
    assert_eq!(SourceKindV1::ProjectionAdmin as u8, 5);
    assert_eq!(SourceKindV1::Administrative as u8, 6);
}

#[test]
fn typed_status_consumer_handles_pending_resolving_committed_expired_and_unknown() {
    let receipt = receipt();
    let values = vec![
        TransactionStatusV1::Pending {
            operation_digest: receipt.operation_digest,
            retry_until_micros: receipt.retry_until_micros,
            phase: PendingPhase::Receiving,
        },
        TransactionStatusV1::Resolving {
            operation_digest: receipt.operation_digest,
            retry_until_micros: receipt.retry_until_micros,
            shard_sequence: Some(77),
        },
        TransactionStatusV1::Committed(receipt.clone()),
        TransactionStatusV1::Expired {
            operation_digest: receipt.operation_digest,
            retry_until_micros: receipt.retry_until_micros,
            tombstone_until_micros: 7_000,
        },
        TransactionStatusV1::Unknown,
    ];
    for (status, expected_http) in values.into_iter().zip([202, 202, 200, 410, 404]) {
        roundtrip(&status);
        assert!(!status.status_digest().unwrap().is_zero());
        assert_eq!(status.http_status_code(), expected_http);
    }
}

#[test]
fn cursor_expired_response_contains_the_authenticated_resnapshot_checkpoint() {
    let snapshot = snapshot();
    let digest = snapshot.snapshot.generation_digest().unwrap();
    let disposition = cursor_disposition(8, 10, 12, digest).unwrap();
    match disposition {
        CursorDispositionV1::Resnapshot(response) => {
            assert_eq!(response.minimum_retained_sequence, 10);
            assert_eq!(response.authenticated_snapshot_digest, digest);
            assert_eq!(
                CursorExpiredV1::decode_canonical(&response.encode_canonical().unwrap()).unwrap(),
                response
            );
        }
        CursorDispositionV1::ReplayFrom { .. } => panic!("stale cursor must resnapshot"),
    }
}
