use super::{
    LifecycleStore, ProposeMemoryRequest, RecordMemoryRequest, TransitionMetadata, accept_memory,
    archive_memory, clear_projection_cache, latest_state_by_scope, latest_state_by_state,
    ledger_file_name, lifecycle_root_from_config, pending_review_entries, project_latest_state,
    promote_memory_to_canonical, propose_ai_memory, read_events_for_record, read_projection,
    read_projection_with_cache_hit, read_projection_with_source, record_manual_memory,
    review_queue_for_scope, wakeup_ready_entries, wakeup_ready_for_scope,
};
use crate::domain::{MemoryLedgerAction, MemoryLifecycleState, MemoryScope, MemorySourceKind};
use std::fs;
use std::path::Path;
use tempfile::tempdir;

#[test]
fn lifecycle_store_should_append_and_read_entries() {
    let temp = tempdir().unwrap();
    let store = LifecycleStore::new(temp.path());

    let manual = record_manual_memory(
        &store,
        RecordMemoryRequest {
            title: "简洁输出".to_string(),
            summary: "用户偏好简洁回复".to_string(),
            memory_type: "preference".to_string(),
            scope: MemoryScope::User,
            source_ref: "manual:gui".to_string(),
            project_id: None,
            user_id: Some("long".to_string()),
            sensitivity: Some("internal".to_string()),
            metadata: TransitionMetadata {
                actor: Some("long".to_string()),
                reason: Some("manual capture".to_string()),
                evidence_refs: vec!["obsidian://preferences".to_string()],
            },
            entities: Vec::new(),
            tags: Vec::new(),
            triggers: Vec::new(),
            related_files: Vec::new(),
            related_records: Vec::new(),
            supersedes: None,
            applies_to: Vec::new(),
            valid_until: None,
        },
    )
    .unwrap();

    let proposal = propose_ai_memory(
        &store,
        ProposeMemoryRequest {
            title: "测试偏好".to_string(),
            summary: "先跑 smoke 再收口单测".to_string(),
            memory_type: "workflow".to_string(),
            scope: MemoryScope::User,
            source_ref: "session:1".to_string(),
            project_id: Some("spool".to_string()),
            user_id: Some("long".to_string()),
            sensitivity: None,
            metadata: TransitionMetadata::default(),
            entities: Vec::new(),
            tags: Vec::new(),
            triggers: Vec::new(),
            related_files: Vec::new(),
            related_records: Vec::new(),
            supersedes: None,
            applies_to: Vec::new(),
            valid_until: None,
        },
    )
    .unwrap();

    let entries = store.read_all().unwrap();
    assert_eq!(entries.len(), 2);
    assert_eq!(entries[0].record.state, MemoryLifecycleState::Accepted);
    assert_eq!(entries[1].record.state, MemoryLifecycleState::Candidate);
    assert_eq!(entries[0].source_kind, MemorySourceKind::Manual);
    assert_eq!(entries[1].source_kind, MemorySourceKind::AiProposal);
    assert_eq!(entries[0].action, MemoryLedgerAction::RecordManual);
    assert_eq!(entries[1].action, MemoryLedgerAction::ProposeAi);
    assert_eq!(entries[0].schema_version, "memory-ledger.v1");
    assert_eq!(manual.scope_key, "long");
    assert_eq!(proposal.scope_key, "long");
    assert_eq!(proposal.record.project_id.as_deref(), Some("spool"));
    assert_eq!(manual.record.sensitivity.as_deref(), Some("internal"));
    assert_eq!(manual.metadata.actor.as_deref(), Some("long"));
    assert_eq!(manual.metadata.reason.as_deref(), Some("manual capture"));
    assert_eq!(manual.metadata.evidence_refs.len(), 1);
    assert_ne!(manual.record_id, proposal.record_id);
}

#[test]
fn lifecycle_store_should_return_empty_when_ledger_missing() {
    let temp = tempdir().unwrap();
    let store = LifecycleStore::new(temp.path());
    assert!(store.read_all().unwrap().is_empty());
}

#[test]
fn lifecycle_helpers_should_expose_default_paths() {
    let root = lifecycle_root_from_config(Path::new("/tmp/config"));
    assert_eq!(root, Path::new("/tmp/config/.spool"));
    assert_eq!(ledger_file_name(), "memory-ledger.jsonl");
}

#[test]
fn lifecycle_transitions_should_append_events_and_project_latest_state() {
    let temp = tempdir().unwrap();
    let store = LifecycleStore::new(temp.path());
    let proposal = propose_ai_memory(
        &store,
        ProposeMemoryRequest {
            title: "测试偏好".to_string(),
            summary: "先跑 smoke 再收口单测".to_string(),
            memory_type: "workflow".to_string(),
            scope: MemoryScope::User,
            source_ref: "session:1".to_string(),
            project_id: None,
            user_id: Some("long".to_string()),
            sensitivity: None,
            metadata: TransitionMetadata::default(),
            entities: Vec::new(),
            tags: Vec::new(),
            triggers: Vec::new(),
            related_files: Vec::new(),
            related_records: Vec::new(),
            supersedes: None,
            applies_to: Vec::new(),
            valid_until: None,
        },
    )
    .unwrap();

    let accepted = accept_memory(&store, &proposal.record_id).unwrap();
    let canonical = promote_memory_to_canonical(&store, &proposal.record_id).unwrap();
    let archived = archive_memory(&store, &proposal.record_id).unwrap();

    let events = read_events_for_record(&store, &proposal.record_id).unwrap();
    assert_eq!(events.len(), 4);
    assert_eq!(events[0].action, MemoryLedgerAction::ProposeAi);
    assert_eq!(events[1].action, MemoryLedgerAction::Accept);
    assert_eq!(events[2].action, MemoryLedgerAction::PromoteToCanonical);
    assert_eq!(events[3].action, MemoryLedgerAction::Archive);
    assert_eq!(accepted.record_id, proposal.record_id);
    assert_eq!(canonical.record_id, proposal.record_id);
    assert_eq!(archived.record_id, proposal.record_id);

    let latest = project_latest_state(&store, &proposal.record_id)
        .unwrap()
        .unwrap();
    assert_eq!(latest.state, MemoryLifecycleState::Archived);
}

#[test]
fn invalid_transition_should_not_append_event() {
    let temp = tempdir().unwrap();
    let store = LifecycleStore::new(temp.path());
    let accepted = record_manual_memory(
        &store,
        RecordMemoryRequest {
            title: "简洁输出".to_string(),
            summary: "用户偏好简洁回复".to_string(),
            memory_type: "preference".to_string(),
            scope: MemoryScope::User,
            source_ref: "manual:gui".to_string(),
            project_id: None,
            user_id: Some("long".to_string()),
            sensitivity: None,
            metadata: TransitionMetadata::default(),
            entities: Vec::new(),
            tags: Vec::new(),
            triggers: Vec::new(),
            related_files: Vec::new(),
            related_records: Vec::new(),
            supersedes: None,
            applies_to: Vec::new(),
            valid_until: None,
        },
    )
    .unwrap();

    let result = accept_memory(&store, &accepted.record_id);
    assert!(result.is_err());
    assert_eq!(
        read_events_for_record(&store, &accepted.record_id)
            .unwrap()
            .len(),
        1
    );
}

#[test]
fn lifecycle_projections_should_use_latest_state() {
    let temp = tempdir().unwrap();
    let store = LifecycleStore::new(temp.path());

    let manual = record_manual_memory(
        &store,
        RecordMemoryRequest {
            title: "简洁输出".to_string(),
            summary: "用户偏好简洁回复".to_string(),
            memory_type: "preference".to_string(),
            scope: MemoryScope::User,
            source_ref: "manual:gui".to_string(),
            project_id: None,
            user_id: Some("long".to_string()),
            sensitivity: None,
            metadata: TransitionMetadata::default(),
            entities: Vec::new(),
            tags: Vec::new(),
            triggers: Vec::new(),
            related_files: Vec::new(),
            related_records: Vec::new(),
            supersedes: None,
            applies_to: Vec::new(),
            valid_until: None,
        },
    )
    .unwrap();

    let proposal = propose_ai_memory(
        &store,
        ProposeMemoryRequest {
            title: "测试偏好".to_string(),
            summary: "先跑 smoke 再收口单测".to_string(),
            memory_type: "workflow".to_string(),
            scope: MemoryScope::User,
            source_ref: "session:1".to_string(),
            project_id: None,
            user_id: Some("long".to_string()),
            sensitivity: None,
            metadata: TransitionMetadata::default(),
            entities: Vec::new(),
            tags: Vec::new(),
            triggers: Vec::new(),
            related_files: Vec::new(),
            related_records: Vec::new(),
            supersedes: None,
            applies_to: Vec::new(),
            valid_until: None,
        },
    )
    .unwrap();

    assert_eq!(pending_review_entries(&store).unwrap().len(), 1);
    assert_eq!(wakeup_ready_entries(&store).unwrap().len(), 1);

    let _ = accept_memory(&store, &proposal.record_id).unwrap();
    assert_eq!(pending_review_entries(&store).unwrap().len(), 0);
    assert_eq!(wakeup_ready_entries(&store).unwrap().len(), 2);

    let _ = archive_memory(&store, &manual.record_id).unwrap();
    assert_eq!(wakeup_ready_entries(&store).unwrap().len(), 1);
}

#[test]
fn lifecycle_queries_should_filter_by_scope_and_state() {
    let temp = tempdir().unwrap();
    let store = LifecycleStore::new(temp.path());

    let _ = record_manual_memory(
        &store,
        RecordMemoryRequest {
            title: "用户偏好".to_string(),
            summary: "偏好简洁".to_string(),
            memory_type: "preference".to_string(),
            scope: MemoryScope::User,
            source_ref: "manual:gui".to_string(),
            project_id: None,
            user_id: Some("long".to_string()),
            sensitivity: None,
            metadata: TransitionMetadata::default(),
            entities: Vec::new(),
            tags: Vec::new(),
            triggers: Vec::new(),
            related_files: Vec::new(),
            related_records: Vec::new(),
            supersedes: None,
            applies_to: Vec::new(),
            valid_until: None,
        },
    )
    .unwrap();

    let project = propose_ai_memory(
        &store,
        ProposeMemoryRequest {
            title: "项目约束".to_string(),
            summary: "保持本地优先".to_string(),
            memory_type: "constraint".to_string(),
            scope: MemoryScope::Project,
            source_ref: "session:2".to_string(),
            project_id: Some("spool".to_string()),
            user_id: None,
            sensitivity: None,
            metadata: TransitionMetadata::default(),
            entities: Vec::new(),
            tags: Vec::new(),
            triggers: Vec::new(),
            related_files: Vec::new(),
            related_records: Vec::new(),
            supersedes: None,
            applies_to: Vec::new(),
            valid_until: None,
        },
    )
    .unwrap();

    assert_eq!(
        latest_state_by_scope(&store, MemoryScope::User, "long")
            .unwrap()
            .len(),
        1
    );
    assert_eq!(
        latest_state_by_scope(&store, MemoryScope::Project, "spool")
            .unwrap()
            .len(),
        1
    );
    assert_eq!(
        latest_state_by_state(&store, MemoryLifecycleState::Candidate)
            .unwrap()
            .len(),
        1
    );
    assert_eq!(
        review_queue_for_scope(&store, MemoryScope::Project, "spool")
            .unwrap()
            .len(),
        1
    );
    assert_eq!(
        wakeup_ready_for_scope(&store, MemoryScope::Project, "spool")
            .unwrap()
            .len(),
        0
    );

    let _ = accept_memory(&store, &project.record_id).unwrap();
    assert_eq!(
        review_queue_for_scope(&store, MemoryScope::Project, "spool")
            .unwrap()
            .len(),
        0
    );
    assert_eq!(
        wakeup_ready_for_scope(&store, MemoryScope::Project, "spool")
            .unwrap()
            .len(),
        1
    );
}

#[test]
fn in_memory_projection_should_reuse_single_snapshot_for_queries() {
    clear_projection_cache();
    let temp = tempdir().unwrap();
    let store = LifecycleStore::new(temp.path());

    let proposal = propose_ai_memory(
        &store,
        ProposeMemoryRequest {
            title: "测试偏好".to_string(),
            summary: "先跑 smoke 再收口单测".to_string(),
            memory_type: "workflow".to_string(),
            scope: MemoryScope::User,
            source_ref: "session:1".to_string(),
            project_id: None,
            user_id: Some("long".to_string()),
            sensitivity: None,
            metadata: TransitionMetadata::default(),
            entities: Vec::new(),
            tags: Vec::new(),
            triggers: Vec::new(),
            related_files: Vec::new(),
            related_records: Vec::new(),
            supersedes: None,
            applies_to: Vec::new(),
            valid_until: None,
        },
    )
    .unwrap();
    let _ = accept_memory(&store, &proposal.record_id).unwrap();

    let projection = read_projection(&store).unwrap();
    assert_eq!(projection.latest_entries().len(), 1);
    assert_eq!(
        projection
            .latest_by_record_id(&proposal.record_id)
            .unwrap()
            .record
            .state,
        MemoryLifecycleState::Accepted
    );
    assert_eq!(projection.pending_review().len(), 0);
    assert_eq!(projection.wakeup_ready().len(), 1);
}

#[test]
fn projection_cache_should_reuse_projection_for_repeated_reads() {
    clear_projection_cache();
    let temp = tempdir().unwrap();
    let store = LifecycleStore::new(temp.path());

    let _ = propose_ai_memory(
        &store,
        ProposeMemoryRequest {
            title: "测试偏好".to_string(),
            summary: "先跑 smoke 再收口单测".to_string(),
            memory_type: "workflow".to_string(),
            scope: MemoryScope::User,
            source_ref: "session:1".to_string(),
            project_id: None,
            user_id: Some("long".to_string()),
            sensitivity: None,
            metadata: TransitionMetadata::default(),
            entities: Vec::new(),
            tags: Vec::new(),
            triggers: Vec::new(),
            related_files: Vec::new(),
            related_records: Vec::new(),
            supersedes: None,
            applies_to: Vec::new(),
            valid_until: None,
        },
    )
    .unwrap();

    let (_first, first_hit) = read_projection_with_cache_hit(&store).unwrap();
    let (_second, second_hit) = read_projection_with_cache_hit(&store).unwrap();
    assert!(!first_hit);
    assert!(second_hit);
}

#[test]
fn projection_cache_should_invalidate_after_append_and_stay_fresh() {
    clear_projection_cache();
    let temp = tempdir().unwrap();
    let store = LifecycleStore::new(temp.path());

    let proposal = propose_ai_memory(
        &store,
        ProposeMemoryRequest {
            title: "测试偏好".to_string(),
            summary: "先跑 smoke 再收口单测".to_string(),
            memory_type: "workflow".to_string(),
            scope: MemoryScope::User,
            source_ref: "session:1".to_string(),
            project_id: None,
            user_id: Some("long".to_string()),
            sensitivity: None,
            metadata: TransitionMetadata::default(),
            entities: Vec::new(),
            tags: Vec::new(),
            triggers: Vec::new(),
            related_files: Vec::new(),
            related_records: Vec::new(),
            supersedes: None,
            applies_to: Vec::new(),
            valid_until: None,
        },
    )
    .unwrap();

    let (first, first_hit) = read_projection_with_cache_hit(&store).unwrap();
    assert_eq!(
        first
            .latest_by_record_id(&proposal.record_id)
            .unwrap()
            .record
            .state,
        MemoryLifecycleState::Candidate
    );
    assert!(!first_hit);

    let _ = accept_memory(&store, &proposal.record_id).unwrap();
    let (second, second_hit) = read_projection_with_cache_hit(&store).unwrap();
    assert_eq!(
        second
            .latest_by_record_id(&proposal.record_id)
            .unwrap()
            .record
            .state,
        MemoryLifecycleState::Accepted
    );
    assert!(!second_hit);
}

#[test]
fn persistent_projection_snapshot_should_be_used_after_memory_cache_clears() {
    clear_projection_cache();
    let temp = tempdir().unwrap();
    let store = LifecycleStore::new(temp.path());

    let proposal = propose_ai_memory(
        &store,
        ProposeMemoryRequest {
            title: "测试偏好".to_string(),
            summary: "先跑 smoke 再收口单测".to_string(),
            memory_type: "workflow".to_string(),
            scope: MemoryScope::User,
            source_ref: "session:1".to_string(),
            project_id: None,
            user_id: Some("long".to_string()),
            sensitivity: None,
            metadata: TransitionMetadata::default(),
            entities: Vec::new(),
            tags: Vec::new(),
            triggers: Vec::new(),
            related_files: Vec::new(),
            related_records: Vec::new(),
            supersedes: None,
            applies_to: Vec::new(),
            valid_until: None,
        },
    )
    .unwrap();

    let (_first, first_source) = read_projection_with_source(&store).unwrap();
    assert_eq!(first_source, "rebuilt");
    assert!(store.projection_snapshot_path().exists());

    clear_projection_cache();
    let (second, second_source) = read_projection_with_source(&store).unwrap();
    assert_eq!(second_source, "persistent");
    assert_eq!(
        second
            .latest_by_record_id(&proposal.record_id)
            .unwrap()
            .record
            .state,
        MemoryLifecycleState::Candidate
    );
}

#[test]
fn corrupted_persistent_projection_snapshot_should_fallback_to_rebuild() {
    clear_projection_cache();
    let temp = tempdir().unwrap();
    let store = LifecycleStore::new(temp.path());

    let _ = propose_ai_memory(
        &store,
        ProposeMemoryRequest {
            title: "测试偏好".to_string(),
            summary: "先跑 smoke 再收口单测".to_string(),
            memory_type: "workflow".to_string(),
            scope: MemoryScope::User,
            source_ref: "session:1".to_string(),
            project_id: None,
            user_id: Some("long".to_string()),
            sensitivity: None,
            metadata: TransitionMetadata::default(),
            entities: Vec::new(),
            tags: Vec::new(),
            triggers: Vec::new(),
            related_files: Vec::new(),
            related_records: Vec::new(),
            supersedes: None,
            applies_to: Vec::new(),
            valid_until: None,
        },
    )
    .unwrap();

    let (_first, first_source) = read_projection_with_source(&store).unwrap();
    assert_eq!(first_source, "rebuilt");
    fs::write(store.projection_snapshot_path(), b"{not-json").unwrap();

    clear_projection_cache();
    let (_second, second_source) = read_projection_with_source(&store).unwrap();
    assert_eq!(second_source, "rebuilt");
}

#[test]
fn lifecycle_store_should_deserialize_legacy_entries_without_metadata() {
    let temp = tempdir().unwrap();
    let store = LifecycleStore::new(temp.path());
    fs::write(
        store.ledger_path(),
        r#"{"schema_version":"memory-ledger.v1","recorded_at":"unix:1","record_id":"legacy-1","scope_key":"long","action":"propose_ai","source_kind":"ai_proposal","record":{"title":"测试偏好","summary":"先 smoke 再收口","memory_type":"workflow","scope":"user","state":"candidate","origin":{"source_kind":"ai_proposal","source_ref":"session:1"},"project_id":null,"user_id":"long","sensitivity":null}}"#,
    )
    .unwrap();

    let entries = store.read_all().unwrap();
    assert_eq!(entries.len(), 1);
    assert!(entries[0].metadata.actor.is_none());
    assert!(entries[0].metadata.reason.is_none());
    assert!(entries[0].metadata.evidence_refs.is_empty());
}

#[test]
fn read_all_should_skip_malformed_lines_without_dropping_valid_entries() {
    // Bug #5: a single corrupted line (incomplete write after SIGKILL,
    // hand-edit, git merge marker, …) MUST NOT take the entire query
    // surface offline. read_all keeps every parseable entry and skips
    // malformed lines (with a stderr warn — invisible from this test
    // but required by the contract).
    let temp = tempdir().unwrap();
    let store = LifecycleStore::new(temp.path());

    // Record one good entry first so we have a known-valid line.
    let good = record_manual_memory(
        &store,
        RecordMemoryRequest {
            title: "good".to_string(),
            summary: "valid line".to_string(),
            memory_type: "preference".to_string(),
            scope: MemoryScope::User,
            source_ref: "manual:test".to_string(),
            project_id: None,
            user_id: Some("long".to_string()),
            sensitivity: None,
            metadata: TransitionMetadata::default(),
            entities: Vec::new(),
            tags: Vec::new(),
            triggers: Vec::new(),
            related_files: Vec::new(),
            related_records: Vec::new(),
            supersedes: None,
            applies_to: Vec::new(),
            valid_until: None,
        },
    )
    .unwrap();

    // Append three corrupt lines + one more good entry, simulating
    // a partial-write SIGKILL followed by a recovery append.
    use std::io::Write;
    let mut file = std::fs::OpenOptions::new()
        .append(true)
        .open(store.ledger_path())
        .unwrap();
    writeln!(file, "{{\"truncated\": ").unwrap();
    writeln!(file, "this is not json at all").unwrap();
    writeln!(file, "<<<<<<< HEAD").unwrap();
    drop(file);

    clear_projection_cache();
    let recovered = record_manual_memory(
        &store,
        RecordMemoryRequest {
            title: "recovered".to_string(),
            summary: "after corruption".to_string(),
            memory_type: "preference".to_string(),
            scope: MemoryScope::User,
            source_ref: "manual:test".to_string(),
            project_id: None,
            user_id: Some("long".to_string()),
            sensitivity: None,
            metadata: TransitionMetadata::default(),
            entities: Vec::new(),
            tags: Vec::new(),
            triggers: Vec::new(),
            related_files: Vec::new(),
            related_records: Vec::new(),
            supersedes: None,
            applies_to: Vec::new(),
            valid_until: None,
        },
    )
    .unwrap();

    let entries = store.read_all().unwrap();
    assert_eq!(entries.len(), 2, "two valid entries must survive");
    let ids: Vec<&str> = entries.iter().map(|e| e.record_id.as_str()).collect();
    assert!(ids.contains(&good.record_id.as_str()));
    assert!(ids.contains(&recovered.record_id.as_str()));
}

#[test]
fn read_all_should_return_empty_when_every_line_is_malformed() {
    // Edge case for Bug #5: even when EVERY line is corrupt we don't
    // bubble an Err. The query surface stays operational and returns
    // an empty projection (the next valid append re-establishes
    // state).
    let temp = tempdir().unwrap();
    let store = LifecycleStore::new(temp.path());
    fs::write(
        store.ledger_path(),
        "this is not json\n{ also broken \nyet another bad line\n",
    )
    .unwrap();
    let entries = store.read_all().unwrap();
    assert!(entries.is_empty());
}
