// SPDX-License-Identifier: BUSL-1.1

use super::*;
use crate::CrdtAuthContext;
use crate::constraint::ConstraintSet;
use crate::dead_letter::CompensationHint;
use crate::error::CrdtError;
use crate::policy::{PolicyResolution, ResolvedAction};
use crate::state::CrdtState;
use loro::LoroValue;

fn setup() -> (CrdtState, ConstraintSet) {
    let state = CrdtState::new(1).unwrap();
    let mut cs = ConstraintSet::new();
    cs.add_unique("users_email_unique", "users", "email");
    cs.add_not_null("users_name_nn", "users", "name");
    cs.add_foreign_key("posts_author_fk", "posts", "author_id", "users", "id");
    (state, cs)
}

#[test]
fn valid_insert_accepted() {
    let (state, cs) = setup();
    let validator = Validator::new(cs, 100);

    let change = ProposedChange {
        collection: "users".into(),
        row_id: "u1".into(),
        surrogate: nodedb_types::Surrogate::ZERO,
        fields: vec![
            ("name".into(), LoroValue::String("Alice".into())),
            (
                "email".into(),
                LoroValue::String("alice@example.com".into()),
            ),
        ],
    };

    assert!(matches!(
        validator.validate(&state, &change),
        ValidationOutcome::Accepted
    ));
}

#[test]
fn not_null_violation() {
    let (_state, cs) = setup();
    let validator = Validator::new(cs, 100);
    let state = CrdtState::new(1).unwrap();

    let change = ProposedChange {
        collection: "users".into(),
        row_id: "u1".into(),
        surrogate: nodedb_types::Surrogate::ZERO,
        fields: vec![(
            "email".into(),
            LoroValue::String("alice@example.com".into()),
        )],
    };

    match validator.validate(&state, &change) {
        ValidationOutcome::Rejected(v) => {
            assert_eq!(v.len(), 1);
            assert_eq!(v[0].constraint_name, "users_name_nn");
            assert!(matches!(
                v[0].hint,
                CompensationHint::ProvideRequiredField { .. }
            ));
        }
        _ => panic!("expected rejection"),
    }
}

#[test]
fn foreign_key_violation() {
    let (state, cs) = setup();
    let validator = Validator::new(cs, 100);

    let change = ProposedChange {
        collection: "posts".into(),
        row_id: "p1".into(),
        surrogate: nodedb_types::Surrogate::ZERO,
        fields: vec![("author_id".into(), LoroValue::String("u1".into()))],
    };

    match validator.validate(&state, &change) {
        ValidationOutcome::Rejected(v) => {
            assert_eq!(v[0].constraint_name, "posts_author_fk");
            assert!(matches!(
                v[0].hint,
                CompensationHint::CreateReferencedRow { .. }
            ));
        }
        _ => panic!("expected FK violation"),
    }
}

#[test]
fn foreign_key_passes_when_parent_exists() {
    let (state, cs) = setup();
    let validator = Validator::new(cs, 100);

    state
        .upsert(
            "users",
            "u1",
            &[
                ("name", LoroValue::String("Alice".into())),
                ("email", LoroValue::String("a@b.com".into())),
            ],
        )
        .unwrap();

    let change = ProposedChange {
        collection: "posts".into(),
        row_id: "p1".into(),
        surrogate: nodedb_types::Surrogate::ZERO,
        fields: vec![("author_id".into(), LoroValue::String("u1".into()))],
    };

    assert!(matches!(
        validator.validate(&state, &change),
        ValidationOutcome::Accepted
    ));
}

#[test]
fn validate_or_reject_enqueues_to_dlq() {
    let (state, cs) = setup();
    let policies = crate::policy::PolicyRegistry::new();
    let mut validator = Validator::new_with_policies(cs, 100, policies, 100);

    let strict_policy = crate::policy::CollectionPolicy::strict();
    validator.policies_mut().set("users", strict_policy);

    let change = ProposedChange {
        collection: "users".into(),
        row_id: "u1".into(),
        surrogate: nodedb_types::Surrogate::ZERO,
        fields: vec![("email".into(), LoroValue::String("a@b.com".into()))],
    };

    let err = validator
        .validate_or_reject(
            &state,
            42,
            CrdtAuthContext::default(),
            &change,
            b"delta-bytes".to_vec(),
        )
        .unwrap_err();

    assert!(matches!(err, CrdtError::ConstraintViolation { .. }));
    assert_eq!(validator.dlq().len(), 1);

    let dl = validator.dlq().peek().unwrap();
    assert_eq!(dl.peer_id, 42);
    assert_eq!(dl.violated_constraint, "users_name_nn");
}

#[test]
fn validate_with_policy_last_writer_wins() {
    let (state, cs) = setup();
    let mut policies = crate::policy::PolicyRegistry::new();
    policies.set("users", crate::policy::CollectionPolicy::ephemeral());

    let mut policy = crate::policy::CollectionPolicy::ephemeral();
    policy.not_null = crate::policy::ConflictPolicy::LastWriterWins;
    policies.set("users", policy);

    let mut validator = Validator::new_with_policies(cs, 100, policies, 100);

    let change = ProposedChange {
        collection: "users".into(),
        row_id: "u1".into(),
        surrogate: nodedb_types::Surrogate::ZERO,
        fields: vec![("email".into(), LoroValue::String("a@b.com".into()))],
    };

    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_millis() as u64;

    let resolution = validator
        .validate_with_policy(
            &state,
            42,
            CrdtAuthContext::default(),
            &change,
            b"delta".to_vec(),
            now,
        )
        .unwrap();

    assert!(matches!(
        resolution,
        PolicyResolution::AutoResolved(ResolvedAction::OverwriteExisting)
    ));
}

#[test]
fn validate_with_policy_rename_suffix() {
    let (state, cs) = setup();
    let mut policies = crate::policy::PolicyRegistry::new();
    let mut policy = crate::policy::CollectionPolicy::ephemeral();
    policy.unique = crate::policy::ConflictPolicy::RenameSuffix;
    policies.set("users", policy);

    let mut validator = Validator::new_with_policies(cs, 100, policies, 100);

    state
        .upsert(
            "users",
            "u1",
            &[
                ("name", LoroValue::String("Alice".into())),
                ("email", LoroValue::String("alice@example.com".into())),
            ],
        )
        .unwrap();

    let change = ProposedChange {
        collection: "users".into(),
        row_id: "u2".into(),
        surrogate: nodedb_types::Surrogate::ZERO,
        fields: vec![
            ("name".into(), LoroValue::String("Bob".into())),
            (
                "email".into(),
                LoroValue::String("alice@example.com".into()),
            ),
        ],
    };

    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_millis() as u64;

    let resolution = validator
        .validate_with_policy(
            &state,
            42,
            CrdtAuthContext::default(),
            &change,
            b"delta".to_vec(),
            now,
        )
        .unwrap();

    match resolution {
        PolicyResolution::AutoResolved(ResolvedAction::RenamedField { field, new_value }) => {
            assert_eq!(field, "email");
            assert!(new_value.contains("_1"));
        }
        _ => panic!("expected RenamedField resolution"),
    }
}

#[test]
fn validate_with_policy_cascade_defer() {
    let (state, cs) = setup();
    let mut policies = crate::policy::PolicyRegistry::new();
    let mut policy = crate::policy::CollectionPolicy::ephemeral();
    policy.foreign_key = crate::policy::ConflictPolicy::CascadeDefer {
        max_retries: 3,
        ttl_secs: 60,
    };
    policies.set("posts", policy);

    let mut validator = Validator::new_with_policies(cs, 100, policies, 100);

    let change = ProposedChange {
        collection: "posts".into(),
        row_id: "p1".into(),
        surrogate: nodedb_types::Surrogate::ZERO,
        fields: vec![("author_id".into(), LoroValue::String("u1".into()))],
    };

    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_millis() as u64;

    let resolution = validator
        .validate_with_policy(
            &state,
            42,
            CrdtAuthContext::default(),
            &change,
            b"delta".to_vec(),
            now,
        )
        .unwrap();

    match resolution {
        PolicyResolution::Deferred {
            retry_after_ms,
            attempt,
        } => {
            assert_eq!(attempt, 0);
            assert_eq!(retry_after_ms, 500);
        }
        _ => panic!("expected Deferred resolution"),
    }

    assert_eq!(validator.deferred().len(), 1);
}

#[test]
fn validate_with_policy_escalate_to_dlq() {
    let (state, cs) = setup();
    let mut policies = crate::policy::PolicyRegistry::new();
    let mut policy = crate::policy::CollectionPolicy::ephemeral();
    policy.not_null = crate::policy::ConflictPolicy::EscalateToDlq;
    policies.set("users", policy);

    let mut validator = Validator::new_with_policies(cs, 100, policies, 100);

    let change = ProposedChange {
        collection: "users".into(),
        row_id: "u1".into(),
        surrogate: nodedb_types::Surrogate::ZERO,
        fields: vec![("email".into(), LoroValue::String("a@b.com".into()))],
    };

    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_millis() as u64;

    let resolution = validator
        .validate_with_policy(
            &state,
            42,
            CrdtAuthContext::default(),
            &change,
            b"delta".to_vec(),
            now,
        )
        .unwrap();

    assert!(matches!(resolution, PolicyResolution::Escalate));
    assert_eq!(validator.dlq().len(), 1);
}
