//! v5.1 c3 S2 — 对账纯函数单元测 + wire field 校正实证。
//!
//! `reconcile(issued, events, window_ms)` 把 `kAXFirstResponderChangedNotification`
//! (rawCode = 1018) 焦点变化事件按 ± window_ms 时间窗与 SDK issued ledger 对账。
//! 命中 = attributed(SDK 自发);未命中 = unattributed(外来 user 干预)。
//!
//! case E 用 c2 capstone 真实 attachment(142 events 含 3 个 1018)证 wire
//! field 名称 `rawCode` 跟 swift 端实际序列化对齐 — c2 capstone 初版判错
//! "0 个 1018" 是 jq 字段名打错(`.code` vs `.rawCode`);wire 校正后 SDK
//! 端 deserialize 拿到正确 raw_code = 1018。

use smix_runner_client::RecordedEvent;
use smix_sdk::{CapsuleReconciliation, FOCUS_CHANGE_RAW_CODE, IssuedAction, IssuedKind, reconcile};

fn make_event(raw_code: i32, ts_ms: f64) -> RecordedEvent {
    RecordedEvent {
        raw_code,
        timestamp_ms: ts_ms,
        extra: serde_json::Map::new(),
    }
}

#[test]
fn case_a_all_attributed() {
    let issued = vec![IssuedAction {
        kind: IssuedKind::Tap,
        timestamp_ms: 1000.0,
        target_hint: Some("input-email".into()),
    }];
    let events = vec![make_event(FOCUS_CHANGE_RAW_CODE, 1050.0)];
    let recon = reconcile(&issued, &events, 500);
    assert_eq!(
        recon,
        CapsuleReconciliation {
            issued_count: 1,
            focus_change_count: 1,
            attributed_count: 1,
            unattributed_count: 0,
            unattributed_events: vec![],
        }
    );
}

#[test]
fn case_b_all_unattributed() {
    let issued = vec![];
    let events = vec![
        make_event(FOCUS_CHANGE_RAW_CODE, 1050.0),
        make_event(FOCUS_CHANGE_RAW_CODE, 2000.0),
    ];
    let recon = reconcile(&issued, &events, 500);
    assert_eq!(recon.issued_count, 0);
    assert_eq!(recon.focus_change_count, 2);
    assert_eq!(recon.attributed_count, 0);
    assert_eq!(recon.unattributed_count, 2);
    assert_eq!(recon.unattributed_events.len(), 2);
}

#[test]
fn case_c_mixed() {
    let issued = vec![IssuedAction {
        kind: IssuedKind::Tap,
        timestamp_ms: 1000.0,
        target_hint: None,
    }];
    let events = vec![
        make_event(FOCUS_CHANGE_RAW_CODE, 1050.0), // within 500ms of 1000 → attributed
        make_event(FOCUS_CHANGE_RAW_CODE, 5000.0), // far → unattributed
        make_event(FOCUS_CHANGE_RAW_CODE, 5100.0), // far → unattributed
    ];
    let recon = reconcile(&issued, &events, 500);
    assert_eq!(recon.focus_change_count, 3);
    assert_eq!(recon.attributed_count, 1);
    assert_eq!(recon.unattributed_count, 2);
    assert_eq!(recon.unattributed_events.len(), 2);
}

#[test]
fn case_d_non_1018_ignored() {
    let issued = vec![];
    let events = vec![
        make_event(4002, 1050.0), // userTesting snapshot polling
        make_event(1028, 1060.0), // raw HID (would only ever be from XCUITest synth)
        make_event(1006, 1070.0), // alert
    ];
    let recon = reconcile(&issued, &events, 500);
    assert_eq!(recon.focus_change_count, 0);
    assert_eq!(recon.attributed_count, 0);
    assert_eq!(recon.unattributed_count, 0);
    assert!(recon.unattributed_events.is_empty());
}

#[test]
fn case_e_c2_capstone_fixture_has_3_x_1018() {
    let json =
        include_str!("../../../docs/plan-history/attachments/v5.1-c2-cap1-probe-events.json");
    let body: serde_json::Value = serde_json::from_str(json).expect("parse fixture");
    let events: Vec<RecordedEvent> =
        serde_json::from_value(body["events"].clone()).expect("deserialize events vec");
    assert_eq!(events.len(), 142, "c2 attachment should carry 142 events");
    let n_1018 = events
        .iter()
        .filter(|e| e.raw_code == FOCUS_CHANGE_RAW_CODE)
        .count();
    assert_eq!(
        n_1018, 3,
        "c2 attachment must have exactly 3 × rawCode=1018; this case pins the wire field-name校正 \
         (capstone 初版 jq .code → 0 命中 是字段错,实际 .rawCode → 3)"
    );
    // 整段对账 — 无 SDK issued,3 个 1018 应该全 unattributed
    let recon = reconcile(&[], &events, 500);
    assert_eq!(recon.focus_change_count, 3);
    assert_eq!(recon.unattributed_count, 3);
}

/// v5.9 c2 — FixtureAction anchor 跟 Tap / CapsuleStart / 任何 IssuedKind
/// 走 reconcile 同源 timestamp 窗口路径。 mark_fixture_action(ts) 后 ±window
/// 内的 1018 focus phantom event 被 attribute 到 mark, unattributed 归 0。
#[test]
fn case_f_fixture_action_attributes_phantom_focus_change() {
    let issued = vec![IssuedAction {
        kind: IssuedKind::FixtureAction("v2-share-present".into()),
        timestamp_ms: 5000.0,
        target_hint: Some("v2-share-present".into()),
    }];
    // V2ShareScreen.triggerShare 同步 record + 然后 present 1.5s 后 fixture-owned
    // auto-dismiss; sim 发 kAXFirstResponderChangedNotification 在 present 完后
    // 几百 ms 内, mark ts + 500ms 后是典型 timing。
    let events = vec![
        make_event(FOCUS_CHANGE_RAW_CODE, 5400.0),
        make_event(FOCUS_CHANGE_RAW_CODE, 6800.0),
    ];
    let recon = reconcile(&issued, &events, 3000);
    // 2 phantom events 都在 mark ts 后 3000ms 窗内
    assert_eq!(
        recon,
        CapsuleReconciliation {
            issued_count: 1,
            focus_change_count: 2,
            attributed_count: 2,
            unattributed_count: 0,
            unattributed_events: vec![],
        }
    );
}

#[test]
fn case_g_fixture_action_outside_window_still_unattributed() {
    let issued = vec![IssuedAction {
        kind: IssuedKind::FixtureAction("v2-share-present".into()),
        timestamp_ms: 5000.0,
        target_hint: Some("v2-share-present".into()),
    }];
    // 1018 event > 3000ms 后 → outside window → unattributed
    let events = vec![make_event(FOCUS_CHANGE_RAW_CODE, 9000.0)];
    let recon = reconcile(&issued, &events, 3000);
    assert_eq!(recon.attributed_count, 0);
    assert_eq!(recon.unattributed_count, 1);
}
