use super::{
    CalibrationAction, CapturePowerCommand, CommandKind, Config, ParseOutcome,
    calibration_request_for, capture_power_request, parse_args_from, parse_args_outcome_from,
};
use lesavka_common::lesavka::{CapturePowerState, UpstreamSyncState};

#[test]
/// Verifies safe recovery commands stay separate from explicit hard reset.
fn command_aliases_parse_to_stable_actions() {
    assert_eq!(CommandKind::parse("status"), Some(CommandKind::Status));
    assert_eq!(CommandKind::parse("get"), Some(CommandKind::Status));
    assert_eq!(CommandKind::parse("version"), Some(CommandKind::Version));
    assert_eq!(CommandKind::parse("versions"), Some(CommandKind::Version));
    assert_eq!(
        CommandKind::parse("calibration"),
        Some(CommandKind::CalibrationStatus)
    );
    assert_eq!(
        CommandKind::parse("calibrate"),
        Some(CommandKind::CalibrationAdjust)
    );
    assert_eq!(
        CommandKind::parse("calibration-restore-default"),
        Some(CommandKind::CalibrationRestoreDefault)
    );
    assert_eq!(
        CommandKind::parse("calibration-restore-factory"),
        Some(CommandKind::CalibrationRestoreFactory)
    );
    assert_eq!(
        CommandKind::parse("calibration-save-default"),
        Some(CommandKind::CalibrationSaveDefault)
    );
    assert_eq!(
        CommandKind::parse("upstream-sync"),
        Some(CommandKind::UpstreamSync)
    );
    assert_eq!(CommandKind::parse("sync"), Some(CommandKind::UpstreamSync));
    assert_eq!(
        CommandKind::parse("output-delay-probe"),
        Some(CommandKind::OutputDelayProbe)
    );
    assert_eq!(
        CommandKind::parse("probe-output-delay"),
        Some(CommandKind::OutputDelayProbe)
    );
    assert_eq!(CommandKind::parse("force-on"), Some(CommandKind::On));
    assert_eq!(CommandKind::parse("force-off"), Some(CommandKind::Off));
    assert_eq!(
        CommandKind::parse("recover-usb"),
        Some(CommandKind::RecoverUsb)
    );
    assert_eq!(
        CommandKind::parse("recover-uac"),
        Some(CommandKind::RecoverUac)
    );
    assert_eq!(CommandKind::parse("heal-av"), Some(CommandKind::RecoverUac));
    assert_eq!(
        CommandKind::parse("heal-upstream"),
        Some(CommandKind::RecoverUac)
    );
    assert_eq!(
        CommandKind::parse("recover-uvc"),
        Some(CommandKind::RecoverUvc)
    );
    assert_eq!(
        CommandKind::parse("hard-reset-usb"),
        Some(CommandKind::ResetUsb)
    );
    assert_eq!(CommandKind::parse("wat"), None);
}

#[test]
fn parse_args_defaults_to_local_status() {
    let config = parse_args_from(std::iter::empty::<&str>()).expect("default config");
    assert_eq!(config.server, "http://127.0.0.1:50051");
    assert_eq!(config.command, CommandKind::Status);
    assert_eq!(config.audio_delta_us, 0);
    assert_eq!(config.video_delta_us, 0);
    assert!(config.note.is_empty());
}

#[test]
fn parse_args_accepts_server_and_command() {
    let config =
        parse_args_from(["--server", " http://lab:50051 ", "upstream-sync"]).expect("config");
    assert_eq!(config.server, "http://lab:50051");
    assert_eq!(config.command, CommandKind::UpstreamSync);
}

#[test]
/// Keeps `parse_args_accepts_output_delay_probe_config` 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 parse_args_accepts_output_delay_probe_config() {
    let config = parse_args_from([
        "--server",
        "http://lab:50051",
        "output-delay-probe",
        "20",
        "4",
        "1000",
        "120",
        "1,2,3,4",
        "0",
        "157712",
    ])
    .expect("probe config");

    assert_eq!(config.command, CommandKind::OutputDelayProbe);
    assert_eq!(config.probe_duration_seconds, 20);
    assert_eq!(config.probe_warmup_seconds, 4);
    assert_eq!(config.probe_pulse_period_ms, 1000);
    assert_eq!(config.probe_pulse_width_ms, 120);
    assert_eq!(config.probe_event_width_codes, "1,2,3,4");
    assert_eq!(config.probe_audio_delay_us, 0);
    assert_eq!(config.probe_video_delay_us, 157_712);
}

#[test]
/// Keeps `parse_args_accepts_calibration_adjustment` 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 parse_args_accepts_calibration_adjustment() {
    let config = parse_args_from([
        "--server",
        "http://lab:50051",
        "calibrate",
        "0",
        "71600",
        "probe",
        "median",
    ])
    .expect("calibration config");
    assert_eq!(config.command, CommandKind::CalibrationAdjust);
    assert_eq!(config.audio_delta_us, 0);
    assert_eq!(config.video_delta_us, 71_600);
    assert_eq!(config.note, "probe median");
}

#[test]
/// Keeps `parse_args_rejects_bad_inputs` 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 parse_args_rejects_bad_inputs() {
    assert!(parse_args_from(["--server"]).is_err());
    assert!(parse_args_from(["nope"]).is_err());
    assert!(parse_args_from(["status", "extra"]).is_err());
    assert!(parse_args_from(["calibrate"]).is_err());
    assert!(parse_args_from(["calibrate", "0", "not-int"]).is_err());
    assert!(
        parse_args_from([
            "output-delay-probe",
            "1",
            "2",
            "3",
            "4",
            "1",
            "0",
            "0",
            "extra"
        ])
        .is_err()
    );
}

#[test]
fn parse_args_reports_help_without_exiting_test_process() {
    assert_eq!(
        parse_args_outcome_from(["--help"]).unwrap(),
        ParseOutcome::Help
    );
    assert_eq!(parse_args_outcome_from(["-h"]).unwrap(), ParseOutcome::Help);
    assert!(parse_args_from(["--help"]).is_err());
}

#[test]
fn parse_args_runtime_wrapper_is_non_panicking_under_tests() {
    let _ = super::parse_args();
}

#[cfg(coverage)]
#[test]
fn coverage_main_references_runtime_parser() {
    super::main();
}

#[cfg(coverage)]
#[tokio::test(flavor = "current_thread")]
async fn coverage_connect_uses_lazy_channel_after_endpoint_validation() {
    let client = super::connect("http://127.0.0.1:1")
        .await
        .expect("coverage lazy channel");
    drop(client);
}

#[test]
/// Keeps status/read commands from accidentally mutating capture power.
fn mutating_commands_map_to_capture_power_requests() {
    let auto = capture_power_request(CommandKind::Auto).expect("auto request");
    assert!(!auto.enabled);
    assert_eq!(auto.command, CapturePowerCommand::Auto as i32);

    let on = capture_power_request(CommandKind::On).expect("on request");
    assert!(on.enabled);
    assert_eq!(on.command, CapturePowerCommand::ForceOn as i32);

    let off = capture_power_request(CommandKind::Off).expect("off request");
    assert!(!off.enabled);
    assert_eq!(off.command, CapturePowerCommand::ForceOff as i32);

    assert!(capture_power_request(CommandKind::Status).is_none());
    assert!(capture_power_request(CommandKind::Version).is_none());
    assert!(capture_power_request(CommandKind::CalibrationStatus).is_none());
    assert!(capture_power_request(CommandKind::CalibrationAdjust).is_none());
    assert!(capture_power_request(CommandKind::CalibrationRestoreDefault).is_none());
    assert!(capture_power_request(CommandKind::CalibrationRestoreFactory).is_none());
    assert!(capture_power_request(CommandKind::CalibrationSaveDefault).is_none());
    assert!(capture_power_request(CommandKind::RecoverUsb).is_none());
    assert!(capture_power_request(CommandKind::RecoverUac).is_none());
    assert!(capture_power_request(CommandKind::RecoverUvc).is_none());
    assert!(capture_power_request(CommandKind::ResetUsb).is_none());
    assert!(capture_power_request(CommandKind::UpstreamSync).is_none());
    assert!(capture_power_request(CommandKind::OutputDelayProbe).is_none());
}

#[test]
fn print_state_accepts_full_capture_power_payload() {
    super::print_state(CapturePowerState {
        available: true,
        enabled: false,
        mode: "auto".to_string(),
        detected_devices: 2,
        active_leases: 1,
        unit: "lesavka-capture-power.service".to_string(),
        detail: "ready".to_string(),
    });
}

#[test]
fn print_versions_accepts_unknown_and_reported_server_identity() {
    super::print_versions(
        "https://lab:50051",
        &lesavka_common::lesavka::HandshakeSet {
            camera_output: "uvc".to_string(),
            camera_codec: "mjpeg".to_string(),
            camera_width: 1280,
            camera_height: 720,
            camera_fps: 30,
            bundled_webcam_media: true,
            ..Default::default()
        },
    );
    super::print_versions(
        "https://lab:50051",
        &lesavka_common::lesavka::HandshakeSet {
            server_version: "0.21.1".to_string(),
            server_revision: "abc1234".to_string(),
            camera_output: "uvc".to_string(),
            camera_codec: "hevc".to_string(),
            camera_width: 1920,
            camera_height: 1080,
            camera_fps: 30,
            bundled_webcam_media: true,
            ..Default::default()
        },
    );
}

#[test]
/// Keeps `print_calibration_accepts_full_payload` 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 print_calibration_accepts_full_payload() {
    super::print_calibration_state(lesavka_common::lesavka::CalibrationState {
        profile: "mjpeg".to_string(),
        factory_audio_offset_us: 0,
        factory_video_offset_us: 1_090_000,
        default_audio_offset_us: 0,
        default_video_offset_us: 1_090_000,
        active_audio_offset_us: 0,
        active_video_offset_us: 1_161_600,
        source: "manual".to_string(),
        confidence: "manual".to_string(),
        updated_at: "2026-05-02T00:00:00Z".to_string(),
        detail: "probe nudge".to_string(),
    });
}

#[test]
fn print_upstream_sync_accepts_complete_and_pending_payloads() {
    super::print_upstream_sync(UpstreamSyncState {
        session_id: 7,
        phase: "locked".to_string(),
        latest_camera_remote_pts_us: Some(1_000),
        latest_microphone_remote_pts_us: Some(1_010),
        last_video_presented_pts_us: Some(2_000),
        last_audio_presented_pts_us: Some(2_010),
        live_lag_ms: Some(44.5),
        planner_skew_ms: Some(-3.25),
        stale_audio_drops: 1,
        stale_video_drops: 2,
        skew_video_drops: 3,
        freshness_reanchors: 4,
        startup_timeouts: 5,
        video_freezes: 6,
        last_reason: "healthy".to_string(),
        client_capture_skew_ms: Some(1.5),
        client_send_skew_ms: Some(-2.5),
        server_receive_skew_ms: Some(3.5),
        camera_client_queue_age_ms: Some(4.5),
        microphone_client_queue_age_ms: Some(5.5),
        camera_server_receive_age_ms: Some(6.5),
        microphone_server_receive_age_ms: Some(7.5),
        client_capture_abs_skew_p95_ms: Some(8.5),
        client_send_abs_skew_p95_ms: Some(9.5),
        server_receive_abs_skew_p95_ms: Some(10.5),
        camera_client_queue_age_p95_ms: Some(11.5),
        microphone_client_queue_age_p95_ms: Some(12.5),
        sink_handoff_skew_ms: Some(-13.5),
        sink_handoff_abs_skew_p95_ms: Some(14.5),
        camera_sink_late_ms: Some(15.5),
        microphone_sink_late_ms: Some(-16.5),
        camera_sink_late_p95_ms: Some(17.5),
        microphone_sink_late_p95_ms: Some(18.5),
        client_timing_window_samples: 19,
        sink_handoff_window_samples: 20,
        uvc_contract_status: "ready".to_string(),
        uvc_transport: "isochronous".to_string(),
        uvc_enforced_frame_cap_bytes: Some(150_000),
        uvc_advertised_frame_bytes: Some(150_000),
        uvc_committed_frame_bytes: Some(150_000),
        uvc_committed_payload_bytes: Some(1024),
        uvc_contract_divergent: false,
        uvc_contract_detail: "live contract agrees".to_string(),
        uvc_contract_generated_unix_ms: Some(1_777_777_777_000),
        uvc_contract_missing_reads: 0,
        uvc_contract_divergent_reads: 0,
        uvc_integrity_status: "ready".to_string(),
        uvc_integrity_detail: "no helper or kernel faults".to_string(),
        uvc_rejected_oversize: 0,
        uvc_rejected_invalid: 0,
        uvc_fallback_idle: 0,
        uvc_held_last_good: 0,
        uvc_strict_validation_failures: 0,
        uvc_dqbuf_errors: 0,
        uvc_qbuf_errors: 0,
        uvc_kernel_errors: 0,
        uvc_integrity_generated_unix_ms: Some(1_777_777_777_000),
        uvc_stale_replay: 0,
        uvc_read_errors: 0,
    });

    super::print_upstream_sync(UpstreamSyncState {
        session_id: 0,
        phase: "acquiring".to_string(),
        last_reason: "waiting".to_string(),
        ..UpstreamSyncState::default()
    });
}

#[test]
/// Keeps `calibration_requests_are_only_built_for_calibration_mutations` 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 calibration_requests_are_only_built_for_calibration_mutations() {
    let config = Config {
        server: "http://127.0.0.1:50051".to_string(),
        command: CommandKind::CalibrationAdjust,
        audio_delta_us: 0,
        video_delta_us: 71_600,
        note: "probe".to_string(),
        probe_duration_seconds: 0,
        probe_warmup_seconds: 0,
        probe_pulse_period_ms: 0,
        probe_pulse_width_ms: 0,
        probe_event_width_codes: String::new(),
        probe_audio_delay_us: 0,
        probe_video_delay_us: 0,
    };
    let request = calibration_request_for(&config).expect("request");
    assert_eq!(request.action, CalibrationAction::AdjustActive as i32);
    assert_eq!(request.audio_delta_us, 0);
    assert_eq!(request.video_delta_us, 71_600);
    assert_eq!(request.note, "probe");

    for (command, action) in [
        (
            CommandKind::CalibrationRestoreDefault,
            CalibrationAction::RestoreDefault,
        ),
        (
            CommandKind::CalibrationRestoreFactory,
            CalibrationAction::RestoreFactory,
        ),
        (
            CommandKind::CalibrationSaveDefault,
            CalibrationAction::SaveActiveAsDefault,
        ),
    ] {
        let mutation = Config {
            server: config.server.clone(),
            command,
            audio_delta_us: config.audio_delta_us,
            video_delta_us: config.video_delta_us,
            note: config.note.clone(),
            probe_duration_seconds: config.probe_duration_seconds,
            probe_warmup_seconds: config.probe_warmup_seconds,
            probe_pulse_period_ms: config.probe_pulse_period_ms,
            probe_pulse_width_ms: config.probe_pulse_width_ms,
            probe_event_width_codes: config.probe_event_width_codes.clone(),
            probe_audio_delay_us: config.probe_audio_delay_us,
            probe_video_delay_us: config.probe_video_delay_us,
        };
        let request = calibration_request_for(&mutation).expect("calibration mutation");
        assert_eq!(request.action, action as i32);
    }

    let status = Config {
        command: CommandKind::CalibrationStatus,
        ..config
    };
    assert!(calibration_request_for(&status).is_none());
}
