use super::parse_args;
use lesavka_client::sync_probe::analyze::{
    SyncAnalysisReport, SyncAnalysisVerdict, SyncCalibrationRecommendation, SyncEventPair,
};

#[test]
fn parse_args_accepts_capture_path_and_json_flag() {
    let args = parse_args(["capture.mkv", "--json"]).expect("args");
    assert_eq!(args.capture_path, std::path::PathBuf::from("capture.mkv"));
    assert!(args.emit_json);
    assert_eq!(args.report_dir, None);
}

#[test]
fn parse_args_accepts_report_dir() {
    let args = parse_args(["capture.mkv", "--report-dir", "/tmp/probe"]).expect("args");
    assert_eq!(args.capture_path, std::path::PathBuf::from("capture.mkv"));
    assert_eq!(
        args.report_dir,
        Some(std::path::PathBuf::from("/tmp/probe"))
    );
}

#[test]
fn parse_args_accepts_event_width_codes() {
    let args = parse_args(["capture.mkv", "--event-width-codes", "1,2,1,3"]).expect("args");
    assert_eq!(args.options.event_width_codes, vec![1, 2, 1, 3]);
}

#[test]
fn parse_args_accepts_analysis_window() {
    let args = parse_args(["capture.mkv", "--analysis-window-s", "8.25:26.5"]).expect("args");
    assert_eq!(args.options.analysis_start_s, Some(8.25));
    assert_eq!(args.options.analysis_end_s, Some(26.5));
}

#[test]
fn parse_args_accepts_inline_options_and_open_ended_windows() {
    let args = parse_args([
        "capture.mkv",
        "--report-dir=/tmp/probe",
        "--event-width-codes=1, 2, 3",
        "--analysis-window-s=:26.5",
    ])
    .expect("args");
    assert_eq!(
        args.report_dir,
        Some(std::path::PathBuf::from("/tmp/probe"))
    );
    assert_eq!(args.options.event_width_codes, vec![1, 2, 3]);
    assert_eq!(args.options.analysis_start_s, None);
    assert_eq!(args.options.analysis_end_s, Some(26.5));

    let args = parse_args(["capture.mkv", "--analysis-window-s=8.25:"]).expect("args");
    assert_eq!(args.options.analysis_start_s, Some(8.25));
    assert_eq!(args.options.analysis_end_s, None);

    let args = parse_args([
        "capture.mkv",
        "--analysis-start-s=1.25",
        "--analysis-end-s",
        "9.5",
    ])
    .expect("args");
    assert_eq!(args.options.analysis_start_s, Some(1.25));
    assert_eq!(args.options.analysis_end_s, Some(9.5));
}

#[test]
fn parse_args_rejects_extra_positional_arguments() {
    assert!(parse_args(["one.mkv", "two.mkv"]).is_err());
    assert!(parse_args(["one.mkv", "--report-dir"]).is_err());
    assert!(parse_args(["one.mkv", "--report-dir="]).is_err());
    assert!(parse_args(["one.mkv", "--event-width-codes"]).is_err());
    assert!(parse_args(["one.mkv", "--event-width-codes", ""]).is_err());
    assert!(parse_args(["one.mkv", "--event-width-codes="]).is_err());
    assert!(parse_args(["one.mkv", "--event-width-codes", "0"]).is_err());
    assert!(parse_args(["one.mkv", "--analysis-window-s"]).is_err());
    assert!(parse_args(["one.mkv", "--analysis-window-s", "wat:10"]).is_err());
    assert!(parse_args(["one.mkv", "--analysis-window-s", "10"]).is_err());
    assert!(parse_args(["one.mkv", "--analysis-window-s="]).is_err());
    assert!(parse_args(["one.mkv", "--analysis-start-s"]).is_err());
    assert!(parse_args(["one.mkv", "--analysis-start-s", "-1"]).is_err());
    assert!(parse_args(["one.mkv", "--analysis-start-s="]).is_err());
    assert!(parse_args(["one.mkv", "--analysis-end-s"]).is_err());
    assert!(parse_args(["one.mkv", "--analysis-end-s=wat"]).is_err());
    assert!(parse_args(["one.mkv", "--analysis-end-s="]).is_err());
}

#[test]
fn parse_args_requires_a_capture_path() {
    let error = parse_args(["--json"]).expect_err("missing capture path should fail");
    assert!(
        error.to_string().contains("capture path is required"),
        "unexpected error: {error:#}"
    );
}

#[test]
fn coverage_main_stub_is_non_panicking() {
    let _ = super::main();
}

#[test]
/// Keeps `human_report_explains_coded_raw_activity_and_unpaired_onsets` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn human_report_explains_coded_raw_activity_and_unpaired_onsets() {
    let report = SyncAnalysisReport {
        video_event_count: 3,
        audio_event_count: 3,
        paired_event_count: 1,
        coded_events: true,
        activity_start_delta_ms: -3_620.7,
        raw_first_video_activity_s: 9.361,
        raw_first_audio_activity_s: 5.740,
        first_skew_ms: -188.4,
        last_skew_ms: -188.4,
        mean_skew_ms: -188.4,
        median_skew_ms: -188.4,
        max_abs_skew_ms: 188.4,
        drift_ms: 0.0,
        skews_ms: vec![-188.4],
        video_onsets_s: vec![9.461, 11.420, 13.367],
        audio_onsets_s: vec![9.135, 11.146, 13.135],
        paired_events: vec![SyncEventPair {
            event_id: 0,
            server_event_id: None,
            event_code: None,
            video_time_s: 11.420,
            audio_time_s: 11.146,
            skew_ms: -188.4,
            confidence: 0.62,
        }],
    };
    let calibration = SyncCalibrationRecommendation {
        ready: false,
        recommended_audio_offset_adjust_us: 0,
        recommended_video_offset_adjust_us: 0,
        note: "need more pairs".to_string(),
    };
    let verdict = SyncAnalysisVerdict {
        status: "gross_failure".to_string(),
        passed: false,
        p95_abs_skew_ms: 188.4,
        max_abs_skew_ms: 188.4,
        preferred_p95_abs_skew_ms: 35.0,
        acceptable_p95_abs_skew_ms: 80.0,
        gross_failure_p95_abs_skew_ms: 250.0,
        catastrophic_max_abs_skew_ms: 1_000.0,
        reason: "coded pulse skew is too high".to_string(),
    };

    let text = super::format_human_report(
        std::path::Path::new("/tmp/capture.webm"),
        &report,
        None,
        &calibration,
        &verdict,
    );

    assert!(text.contains("- evidence mode: coded pulses"));
    assert!(text.contains("raw activity handling: reported only"));
    assert!(text.contains("- paired window first video/audio: 11.420 s / 11.146 s"));
    assert!(text.contains("- unpaired video onsets: 9.461s, 13.367s"));
    assert!(text.contains("- unpaired audio onsets: 9.135s, 13.135s"));
}

#[test]
/// Keeps `signature_coverage_reports_missing_and_unknown_coded_pairs` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn signature_coverage_reports_missing_and_unknown_coded_pairs() {
    let report = SyncAnalysisReport {
        video_event_count: 3,
        audio_event_count: 3,
        paired_event_count: 2,
        coded_events: true,
        activity_start_delta_ms: 0.0,
        raw_first_video_activity_s: 1.0,
        raw_first_audio_activity_s: 1.0,
        first_skew_ms: 0.0,
        last_skew_ms: 0.0,
        mean_skew_ms: 0.0,
        median_skew_ms: 0.0,
        max_abs_skew_ms: 0.0,
        drift_ms: 0.0,
        skews_ms: vec![0.0, 0.0],
        video_onsets_s: vec![1.0, 2.0, 3.0],
        audio_onsets_s: vec![1.0, 2.0, 3.0],
        paired_events: vec![
            SyncEventPair {
                event_id: 0,
                server_event_id: Some(0),
                event_code: Some(1),
                video_time_s: 1.0,
                audio_time_s: 1.0,
                skew_ms: 0.0,
                confidence: 1.0,
            },
            SyncEventPair {
                event_id: 1,
                server_event_id: None,
                event_code: None,
                video_time_s: 2.0,
                audio_time_s: 2.0,
                skew_ms: 0.0,
                confidence: 0.4,
            },
        ],
    };
    let calibration = SyncCalibrationRecommendation {
        ready: false,
        recommended_audio_offset_adjust_us: 0,
        recommended_video_offset_adjust_us: 0,
        note: "need more pairs".to_string(),
    };
    let verdict = SyncAnalysisVerdict {
        status: "insufficient_data".to_string(),
        passed: false,
        p95_abs_skew_ms: 0.0,
        max_abs_skew_ms: 0.0,
        preferred_p95_abs_skew_ms: 35.0,
        acceptable_p95_abs_skew_ms: 80.0,
        gross_failure_p95_abs_skew_ms: 250.0,
        catastrophic_max_abs_skew_ms: 1_000.0,
        reason: "need more pairs".to_string(),
    };
    let coverage = super::signature_coverage(&[1, 2, 3], &report);
    let text = super::format_human_report(
        std::path::Path::new("/tmp/capture.webm"),
        &report,
        coverage.as_ref(),
        &calibration,
        &verdict,
    );

    assert!(text.contains("- expected coded signatures: 3"));
    assert!(text.contains("- paired coded signatures: 1/3"));
    assert!(text.contains("- missing paired signature ids: 1, 2"));
    assert!(text.contains("- missing paired signature codes: 2, 3"));
    assert!(text.contains("- paired signatures without identity: 1"));
}

#[test]
/// Keeps `formatting_helpers_cover_empty_and_truncated_lists` explicit because analyzer text should stay readable when a probe has no extra onsets or many noisy detections.
/// Inputs are empty and oversized helper lists; output is stable operator-facing text.
fn formatting_helpers_cover_empty_and_truncated_lists() {
    let report = SyncAnalysisReport {
        video_event_count: 0,
        audio_event_count: 0,
        paired_event_count: 0,
        coded_events: false,
        activity_start_delta_ms: 0.0,
        raw_first_video_activity_s: 0.0,
        raw_first_audio_activity_s: 0.0,
        first_skew_ms: 0.0,
        last_skew_ms: 0.0,
        mean_skew_ms: 0.0,
        median_skew_ms: 0.0,
        max_abs_skew_ms: 0.0,
        drift_ms: 0.0,
        skews_ms: Vec::new(),
        video_onsets_s: (0..14).map(f64::from).collect(),
        audio_onsets_s: Vec::new(),
        paired_events: Vec::new(),
    };

    assert!(super::signature_coverage(&[], &report).is_none());
    assert_eq!(super::format_usize_list(&[]), "none");
    assert_eq!(super::format_u32_list(&[]), "none");
    assert_eq!(super::format_onset_list(&[]), "none");
    assert!(super::format_onset_list(&report.video_onsets_s).contains("...+2"));
}
