use crate::CONTROL_PROTOCOL_VERSION;
use crate::control_plane::{
    ControlPayload, ControlRequest, ControlRequestEnvelope, ControlResponseEnvelope, HealthView,
};
use crate::tui::{
    CommandConfirmation, OperatorCommand, OperatorScreen, OperatorTui, TuiAction, TuiError,
    TuiFuture, TuiInput, TuiRenderOptions, TuiTransport,
};
use std::sync::Mutex;

struct FakeTransport(Mutex<Vec<ControlRequest>>);

impl FakeTransport {
    fn new() -> Self {
        Self(Mutex::new(Vec::new()))
    }
}

impl TuiTransport for FakeTransport {
    fn request(&self, request: ControlRequestEnvelope) -> TuiFuture<'_> {
        self.0
            .lock()
            .expect("requests")
            .push(request.request.clone());
        Box::pin(async move {
            let result = match request.request {
                ControlRequest::Health => ControlPayload::Health(HealthView {
                    service_state: "running".into(),
                    ready: true,
                    uptime_seconds: 7,
                    protocol_version: CONTROL_PROTOCOL_VERSION,
                }),
                ControlRequest::Runtime => ControlPayload::Runtime(crate::RuntimeView {
                    grid_state: "ready".into(),
                    generation: 2,
                    transport_connected: true,
                    agent_ready: true,
                    region_id: Some("region".into()),
                    region_handle: Some(9_007_199_254_740_992),
                    region_name: Some("Café 世界".into()),
                    position: Some([1.0, 2.0, 3.0]),
                    behavior_mode: "active".into(),
                    control_queue_used: 1,
                    control_queue_capacity: 8,
                    budget_tool_calls_used: 2,
                    budget_movement_millimeters_used: 3,
                    active_build_transaction: Some("build-7".into()),
                    build_progress: Some("configured".into()),
                    build_orphan_ids: Vec::new(),
                    active_visual_capture: None,
                    visual_progress: None,
                    visual_image_sha256: None,
                }),
                ControlRequest::ListSessions { .. } => ControlPayload::Sessions(crate::Page {
                    items: Vec::new(),
                    next_cursor: None,
                }),
                ControlRequest::ListScheduledJobs { .. } => {
                    ControlPayload::ScheduledJobs(crate::Page {
                        items: Vec::new(),
                        next_cursor: None,
                    })
                }
                ControlRequest::ListPendingApprovals { .. } => {
                    ControlPayload::PendingApprovals(crate::Page {
                        items: Vec::new(),
                        next_cursor: None,
                    })
                }
                ControlRequest::ListAuditEvents { .. } => {
                    ControlPayload::AuditEvents(crate::Page {
                        items: Vec::new(),
                        next_cursor: None,
                    })
                }
                ControlRequest::ListObservabilityEvents { .. } => {
                    ControlPayload::ObservabilityEvents(crate::Page {
                        items: Vec::new(),
                        next_cursor: None,
                    })
                }
                ControlRequest::Metrics => ControlPayload::Metrics(empty_metrics()),
                _ => ControlPayload::Completed,
            };
            Ok(ControlResponseEnvelope {
                version: CONTROL_PROTOCOL_VERSION,
                request_id: request.request_id,
                result: Ok(result),
            })
        })
    }
}

fn empty_metrics() -> crate::MetricsSnapshot {
    serde_json::from_value(serde_json::json!({
        "ready":true,"reconnects":0,"active_sessions":0,"active_tasks":0,
        "queue_depth":0,"queue_capacity":8,
        "inference_latency":{"samples":0,"total_millis":0,"maximum_millis":0},
        "tool_latency":{"samples":0,"total_millis":0,"maximum_millis":0},
        "inference_outcomes":{"allowed":0,"denied":0,"approval_required":0,"completed":0,"failed":0,"cancelled":0},
        "tool_outcomes":{"allowed":0,"denied":0,"approval_required":0,"completed":0,"failed":0,"cancelled":0},
        "policy_outcomes":{"allowed":0,"denied":0,"approval_required":0,"completed":0,"failed":0,"cancelled":0},
        "rate_limit_used":0,"rate_limit_capacity":10,"recorded_events":0,
        "dropped_ring_events":0,"dropped_subscriber_events":0,"dropped_journal_events":0,
        "diagnostic_entries":0
    })).expect("metrics fixture")
}

#[tokio::test]
async fn snapshot_refresh_is_transport_neutral_and_bounded() {
    let transport = FakeTransport::new();
    let mut app = OperatorTui::default();
    app.refresh(&transport).await.expect("refresh");
    assert_eq!(
        app.snapshot.health.as_ref().expect("health").service_state,
        "running"
    );
    assert_eq!(transport.0.lock().expect("requests").len(), 8);
}

#[test]
fn every_screen_renders_without_a_terminal_at_small_unicode_and_mono_sizes() {
    let mut app = OperatorTui::default();
    for screen in [
        OperatorScreen::Overview,
        OperatorScreen::Sessions,
        OperatorScreen::QueuesAndBudgets,
        OperatorScreen::Roaming,
        OperatorScreen::Approvals,
        OperatorScreen::Timeline,
        OperatorScreen::Health,
        OperatorScreen::Errors,
        OperatorScreen::Diagnostics,
        OperatorScreen::Preferences,
    ] {
        while app.screen != screen {
            let _ = app.reduce(TuiInput::NextScreen);
        }
        let output = app.render(TuiRenderOptions {
            width: 20,
            height: 5,
            color: false,
        });
        assert!(!output.is_empty());
        assert!(output.lines().count() <= 4);
        assert!(
            output
                .lines()
                .all(|line| unicode_width::UnicodeWidthStr::width(line) <= 20)
        );
    }
}

#[test]
fn navigation_wraps_and_every_management_command_has_risk_confirmation() {
    let mut app = OperatorTui::default();
    assert_eq!(app.reduce(TuiInput::PreviousScreen), TuiAction::None);
    assert_eq!(app.screen, OperatorScreen::Preferences);
    let commands = [
        OperatorCommand::Pause,
        OperatorCommand::Resume,
        OperatorCommand::CancelAction("action-1".into()),
        OperatorCommand::DecideApproval {
            id: 1,
            approve: true,
        },
        OperatorCommand::Reconnect,
        OperatorCommand::ExpireSession {
            avatar_id: "00000000-0000-0000-0000-000000000001".into(),
            direct_im: true,
        },
        OperatorCommand::ToggleSchedule {
            job_id: "default-roaming".into(),
            enabled: false,
        },
        OperatorCommand::Shutdown,
    ];
    for command in commands {
        let confirmation = command.confirmation();
        let action = app.reduce(TuiInput::Command(command.clone()));
        if confirmation == CommandConfirmation::None {
            assert_eq!(action, TuiAction::Execute(command));
        } else {
            assert_eq!(action, TuiAction::None);
            assert_eq!(app.reduce(TuiInput::Confirm), TuiAction::Execute(command));
        }
    }
}

#[test]
fn every_global_command_has_a_keyboard_path() {
    let app = OperatorTui::default();
    assert_eq!(app.command_shortcut('p'), Some(OperatorCommand::Pause));
    assert_eq!(app.command_shortcut('u'), Some(OperatorCommand::Resume));
    assert_eq!(app.command_shortcut('f'), Some(OperatorCommand::Reconnect));
    assert_eq!(app.command_shortcut('s'), Some(OperatorCommand::Shutdown));
}

#[test]
fn transport_errors_are_safe_display_text() {
    struct Broken;
    impl TuiTransport for Broken {
        fn request(&self, _: ControlRequestEnvelope) -> TuiFuture<'_> {
            Box::pin(async { Err(TuiError("transport closed".into())) })
        }
    }
    let runtime = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .expect("runtime");
    let mut app = OperatorTui::default();
    let error = runtime.block_on(app.refresh(&Broken)).expect_err("failure");
    assert_eq!(error.to_string(), "transport closed");
    assert!(
        !app.render(TuiRenderOptions {
            width: 80,
            height: 24,
            color: false
        })
        .contains("token")
    );
}
