use std::fs;

use ratatui::layout::Rect;
use tempfile::tempdir;

use super::*;
use crate::action::Action;
use crate::cli::Cli;
use crate::format::parse::{FieldValue, StructValue};
use crate::format::types::{FieldDef, FieldType};

fn app_with_len(len: usize) -> App {
    let dir = tempdir().unwrap();
    let file = dir.path().join("sample.bin");
    fs::write(&file, vec![0_u8; len]).unwrap();
    let cli = Cli {
        file: Some(file),
        pid: None,
        process: None,
        config: None,
        bytes_per_line: Some(16),
        page_size: Some(4096),
        cache_pages: Some(8),
        profile: false,
        readonly: false,
        no_color: true,
        offset: None,
        inspector: false,
    };
    let mut app = App::from_cli(cli).unwrap();
    app.view_rows = 4;
    app
}

fn app_with_inspector_field_for(format_name: &str) -> App {
    app_with_inspector_field_editable(format_name, true)
}

fn app_with_inspector_field_editable(format_name: &str, editable: bool) -> App {
    let mut app = app_with_len(4);
    let field = FieldDef {
        name: "entry".to_owned(),
        offset: 0,
        field_type: FieldType::U8,
        description: String::new(),
        editable,
    };
    let structs = vec![StructValue {
        name: "Header".to_owned(),
        base_offset: 0,
        fields: vec![FieldValue {
            def: field,
            abs_offset: 0,
            raw_bytes: vec![0],
            display: "0x00".to_owned(),
            size: 1,
        }],
        children: Vec::new(),
    }];
    let collapsed_nodes = std::collections::BTreeSet::new();
    let rows = crate::format::parse::flatten(&structs, &collapsed_nodes);
    app.show_side_panel = true;
    app.active_side_panel = crate::app::SidePanelKind::Inspector;
    app.inspector_state = Some(crate::app::InspectorState {
        format_name: format_name.to_owned(),
        structs,
        rows,
        scroll_offset: 0,
        selected_row: 1,
        editing: None,
        collapsed_nodes,
    });
    app.mode = Mode::SidePanel;
    app
}

fn app_with_inspector_field() -> App {
    app_with_inspector_field_for("TEST")
}

fn type_command(app: &mut App, input: &str) {
    for ch in input.chars() {
        app.handle_action(Action::CommandChar(ch));
    }
}

#[test]
fn command_cursor_navigation_and_editing() {
    // Move and insert in middle
    let mut app = app_with_len(4);
    app.handle_action(Action::EnterCommand);
    app.handle_action(Action::CommandChar('a'));
    app.handle_action(Action::CommandChar('b'));
    app.handle_action(Action::CommandChar('c'));
    app.handle_action(Action::CommandLeft);
    app.handle_action(Action::CommandLeft);
    app.handle_action(Action::CommandChar('X'));
    assert_eq!(app.command_buffer, "aXbc");
    assert_eq!(app.command_cursor_pos, 2);

    // Backspace respects cursor position (separate test)
    let mut app2 = app_with_len(4);
    app2.handle_action(Action::EnterCommand);
    app2.handle_action(Action::CommandChar('a'));
    app2.handle_action(Action::CommandChar('b'));
    app2.handle_action(Action::CommandChar('c'));
    app2.handle_action(Action::CommandLeft);
    app2.handle_action(Action::CommandBackspace);
    assert_eq!(app2.command_buffer, "ac");
    assert_eq!(app2.command_cursor_pos, 1);

    // Delete, home, end
    let mut app3 = app_with_len(4);
    app3.handle_action(Action::EnterCommand);
    type_command(&mut app3, "abcd");
    app3.handle_action(Action::CommandHome);
    assert_eq!(app3.command_cursor_pos, 0);
    app3.handle_action(Action::CommandRight);
    app3.handle_action(Action::CommandRight);
    app3.handle_action(Action::CommandDelete);
    assert_eq!(app3.command_buffer, "abd");
    app3.handle_action(Action::CommandEnd);
    assert_eq!(app3.command_cursor_pos, app3.command_buffer.len());
}

#[test]
fn inspector_enter_escape_and_format_warnings() {
    let mut app = app_with_inspector_field();
    app.handle_action(Action::SidePanelEnter);
    assert_eq!(app.mode, Mode::InspectorEdit);
    assert!(app
        .inspector()
        .and_then(|inspector| inspector.editing.as_ref())
        .is_some());

    app.handle_action(Action::LeaveMode);
    assert_eq!(app.mode, Mode::SidePanel);
    assert!(app
        .inspector()
        .and_then(|inspector| inspector.editing.as_ref())
        .is_none());

    // PNG format warning
    let mut app_png = app_with_inspector_field_for("PNG");
    app_png.handle_action(Action::SidePanelEnter);
    assert_eq!(app_png.status_level, crate::app::StatusLevel::Warning);
    assert!(app_png.status_message.contains("PNG inspector edits"));

    // GZIP format warning
    let mut app_gz = app_with_inspector_field_for("GZIP");
    app_gz.handle_action(Action::SidePanelEnter);
    assert!(app_gz.status_message.contains("GZIP inspector edits"));

    // SQLite format warning
    let mut app_sqlite = app_with_inspector_field_for("SQLite");
    app_sqlite.handle_action(Action::SidePanelEnter);
    assert!(app_sqlite.status_message.contains("SQLite inspector edits"));

    // PCAP/PCAPNG format warnings
    let mut app_pcap = app_with_inspector_field_for("PCAP");
    app_pcap.handle_action(Action::SidePanelEnter);
    assert!(app_pcap.status_message.contains("PCAP inspector edits"));

    let mut app_pcapng = app_with_inspector_field_for("PCAPNG");
    app_pcapng.handle_action(Action::SidePanelEnter);
    assert!(app_pcapng.status_message.contains("PCAPNG inspector edits"));

    // TAR format warning
    let mut app_tar = app_with_inspector_field_for("TAR");
    app_tar.handle_action(Action::SidePanelEnter);
    assert!(app_tar.status_message.contains("TAR inspector edits"));

    // JPEG format warning
    let mut app_jpg = app_with_inspector_field_for("JPEG");
    app_jpg.handle_action(Action::SidePanelEnter);
    assert!(app_jpg.status_message.contains("JPEG inspector edits"));
}

#[test]
fn inspector_warns_when_editing_png_field() {
    let mut app = app_with_inspector_field_for("PNG");

    app.handle_action(Action::SidePanelEnter);

    assert_eq!(app.mode, Mode::InspectorEdit);
    assert_eq!(app.status_level, crate::app::StatusLevel::Warning);
    assert!(app.status_message.contains("PNG inspector edits"));
}

#[test]
fn inspector_warns_when_editing_gzip_field() {
    let mut app = app_with_inspector_field_for("GZIP");

    app.handle_action(Action::SidePanelEnter);

    assert_eq!(app.mode, Mode::InspectorEdit);
    assert_eq!(app.status_level, crate::app::StatusLevel::Warning);
    assert!(app.status_message.contains("GZIP inspector edits"));
}

#[test]
fn inspector_warns_when_editing_sqlite_field() {
    let mut app = app_with_inspector_field_for("SQLite");

    app.handle_action(Action::SidePanelEnter);

    assert_eq!(app.mode, Mode::InspectorEdit);
    assert_eq!(app.status_level, crate::app::StatusLevel::Warning);
    assert!(app.status_message.contains("SQLite inspector edits"));
}

#[test]
fn inspector_warns_when_editing_pcap_field() {
    let mut app = app_with_inspector_field_for("PCAP");

    app.handle_action(Action::SidePanelEnter);

    assert_eq!(app.mode, Mode::InspectorEdit);
    assert_eq!(app.status_level, crate::app::StatusLevel::Warning);
    assert!(app.status_message.contains("PCAP inspector edits"));
}

#[test]
fn inspector_warns_when_editing_pcapng_field() {
    let mut app = app_with_inspector_field_for("PCAPNG");

    app.handle_action(Action::SidePanelEnter);

    assert_eq!(app.mode, Mode::InspectorEdit);
    assert_eq!(app.status_level, crate::app::StatusLevel::Warning);
    assert!(app.status_message.contains("PCAPNG inspector edits"));
}

#[test]
fn inspector_warns_when_editing_gif_field() {
    let mut app = app_with_inspector_field_for("GIF");

    app.handle_action(Action::SidePanelEnter);

    assert_eq!(app.mode, Mode::InspectorEdit);
    assert_eq!(app.status_level, crate::app::StatusLevel::Warning);
    assert!(app.status_message.contains("GIF inspector edits"));
}

#[test]
fn inspector_warns_when_editing_bmp_field() {
    let mut app = app_with_inspector_field_for("BMP");

    app.handle_action(Action::SidePanelEnter);

    assert_eq!(app.mode, Mode::InspectorEdit);
    assert_eq!(app.status_level, crate::app::StatusLevel::Warning);
    assert!(app.status_message.contains("BMP inspector edits"));
}

#[test]
fn inspector_warns_when_editing_wav_field() {
    let mut app = app_with_inspector_field_for("WAV");

    app.handle_action(Action::SidePanelEnter);

    assert_eq!(app.mode, Mode::InspectorEdit);
    assert_eq!(app.status_level, crate::app::StatusLevel::Warning);
    assert!(app.status_message.contains("WAV inspector edits"));
}

#[test]
fn inspector_warns_when_editing_tar_field() {
    let mut app = app_with_inspector_field_for("TAR");

    app.handle_action(Action::SidePanelEnter);

    assert_eq!(app.mode, Mode::InspectorEdit);
    assert_eq!(app.status_level, crate::app::StatusLevel::Warning);
    assert!(app.status_message.contains("TAR inspector edits"));
}

#[test]
fn inspector_warns_when_editing_jpeg_field() {
    let mut app = app_with_inspector_field_for("JPEG");

    app.handle_action(Action::SidePanelEnter);

    assert_eq!(app.mode, Mode::InspectorEdit);
    assert_eq!(app.status_level, crate::app::StatusLevel::Warning);
    assert!(app.status_message.contains("JPEG inspector edits"));
}

#[test]
fn hidden_inspector_focus_falls_back_to_normal_mode() {
    let mut app = app_with_inspector_field();
    app.last_columns = Some(crate::view::layout::MainColumns {
        main_pane_kind: crate::view::layout::MainPaneKind::Hex,
        gutter: Rect::new(0, 0, 4, 4),
        sep1: Rect::new(4, 0, 1, 4),
        hex: Rect::new(5, 0, 20, 4),
        sep2: Rect::new(25, 0, 1, 4),
        ascii: Rect::new(26, 0, 10, 4),
        side_panel_sep: None,
        side_panel: None,
    });
    app.ensure_side_panel_focus_visible();
    assert_eq!(app.mode, Mode::Normal);
    assert!(app.status_message.contains("too narrow"));

    // Showing a hidden inspector warns when the terminal is too narrow.
    let mut app2 = app_with_inspector_field();
    app2.show_side_panel = false;
    app2.mode = Mode::Normal;
    app2.last_columns = app.last_columns;
    app2.handle_action(Action::ToggleSidePanel);
    assert_eq!(app2.mode, Mode::Normal);
    assert!(app2.show_side_panel);
    assert!(app2.status_message.contains("too narrow"));

    // Showing a hidden inspector succeeds when width is sufficient.
    let mut app3 = app_with_inspector_field();
    app3.show_side_panel = false;
    app3.mode = Mode::Normal;
    app3.last_columns = Some(crate::view::layout::MainColumns {
        main_pane_kind: crate::view::layout::MainPaneKind::Hex,
        gutter: Rect::new(0, 0, 8, 4),
        sep1: Rect::new(8, 0, 1, 4),
        hex: Rect::new(9, 0, 90, 4),
        sep2: Rect::new(99, 0, 1, 4),
        ascii: Rect::new(100, 0, 40, 4),
        side_panel_sep: None,
        side_panel: None,
    });
    app3.handle_action(Action::ToggleSidePanel);
    assert!(app3.show_side_panel);
    assert_eq!(app3.mode, Mode::SidePanel);
    assert!(!app3.status_message.contains("too narrow"));

    // No detected format stays in normal with hint
    let mut app4 = app_with_len(8);
    app4.mode = Mode::Normal;
    app4.last_columns = app3.last_columns;
    app4.handle_action(Action::ToggleSidePanel);
    assert_eq!(app4.mode, Mode::Normal);
    assert!(app4.status_message.contains("no format detected"));
    let supported_formats = crate::format::detect::supported_format_display_list();
    assert!(app4.status_message.contains(supported_formats.as_str()));

    // View-only inspector reports read-only mode
    let mut app5 = app_with_inspector_field_editable("TEST", false);
    app5.show_side_panel = false;
    app5.mode = Mode::Normal;
    app5.last_columns = app3.last_columns;
    app5.handle_action(Action::ToggleSidePanel);
    assert!(app5.show_side_panel);
    assert_eq!(app5.mode, Mode::SidePanel);
    assert!(app5.status_message.contains("view-only"));

    // Enter on read-only field reports reason
    let mut app6 = app_with_inspector_field_editable("TEST", false);
    app6.handle_action(Action::SidePanelEnter);
    assert_eq!(app6.mode, Mode::SidePanel);
    assert!(app6.status_message.contains("read-only"));
}

#[test]
fn entering_inspector_without_detected_format_stays_in_normal_with_hint() {
    let mut app = app_with_len(8);
    app.mode = Mode::Normal;
    app.last_columns = Some(crate::view::layout::MainColumns {
        main_pane_kind: crate::view::layout::MainPaneKind::Hex,
        gutter: Rect::new(0, 0, 8, 4),
        sep1: Rect::new(8, 0, 1, 4),
        hex: Rect::new(9, 0, 90, 4),
        sep2: Rect::new(99, 0, 1, 4),
        ascii: Rect::new(100, 0, 40, 4),
        side_panel_sep: None,
        side_panel: None,
    });

    app.handle_action(Action::ToggleSidePanel);

    assert_eq!(app.mode, Mode::Normal);
    assert!(app.show_side_panel);
    assert_eq!(app.status_level, crate::app::StatusLevel::Warning);
    assert!(app.status_message.contains("no format detected"));
    let supported_formats = crate::format::detect::supported_format_display_list();
    assert!(app.status_message.contains(supported_formats.as_str()));
}

#[test]
fn entering_view_only_inspector_reports_read_only_mode() {
    let mut app = app_with_inspector_field_editable("TEST", false);
    app.show_side_panel = false;
    app.mode = Mode::Normal;
    app.last_columns = Some(crate::view::layout::MainColumns {
        main_pane_kind: crate::view::layout::MainPaneKind::Hex,
        gutter: Rect::new(0, 0, 8, 4),
        sep1: Rect::new(8, 0, 1, 4),
        hex: Rect::new(9, 0, 90, 4),
        sep2: Rect::new(99, 0, 1, 4),
        ascii: Rect::new(100, 0, 40, 4),
        side_panel_sep: None,
        side_panel: None,
    });

    app.handle_action(Action::ToggleSidePanel);

    assert!(app.show_side_panel);
    assert_eq!(app.mode, Mode::SidePanel);
    assert_eq!(app.status_level, crate::app::StatusLevel::Info);
    assert!(app.status_message.contains("view-only"));
}

#[test]
fn tab_hides_visible_side_panel_from_normal_focus() {
    let mut app = app_with_inspector_field();
    app.mode = Mode::Normal;

    app.handle_action(Action::ToggleSidePanel);

    assert!(!app.show_side_panel);
    assert_eq!(app.mode, Mode::Normal);
}

#[test]
fn inspector_enter_on_read_only_field_reports_reason() {
    let mut app = app_with_inspector_field_editable("TEST", false);

    app.handle_action(Action::SidePanelEnter);

    assert_eq!(app.mode, Mode::SidePanel);
    assert_eq!(app.status_level, crate::app::StatusLevel::Info);
    assert!(app.status_message.contains("read-only"));
    assert!(app.status_message.contains("entry"));
}

#[test]
fn data_panel_keys_do_not_fall_through_to_inspector_editing() {
    let mut app = app_with_len(8);
    app.execute_command(crate::commands::types::Command::Data)
        .unwrap();

    app.handle_action(Action::SidePanelChar('x'));
    assert_eq!(app.mode, Mode::SidePanel);
    assert!(app.inspector().is_none());

    app.handle_action(Action::SidePanelEnter);
    assert_eq!(app.mode, Mode::Visual);
    assert!(app.selection_range().is_some());
    assert_eq!(
        app.data_state()
            .and_then(|state| state.selected_label.as_deref()),
        Some("binary")
    );
}

#[test]
fn refocusing_inspector_page_keeps_existing_state_without_rebuild() {
    let mut app = app_with_inspector_field();
    if let Some(inspector) = app.inspector_mut() {
        inspector.selected_row = 1;
        inspector.scroll_offset = 3;
    }
    app.show_side_panel = false;
    app.mode = Mode::Normal;
    app.handle_action(Action::ToggleSidePanel);

    let inspector = app.inspector().expect("inspector state preserved");
    assert_eq!(app.mode, Mode::SidePanel);
    assert_eq!(inspector.selected_row, 1);
    assert_eq!(inspector.scroll_offset, 3);
}

#[test]
fn command_mode_success_and_history() {
    // Failed command keeps buffer for editing
    let mut app = app_with_len(1);
    app.mode = Mode::EditHex {
        phase: NibblePhase::High,
    };
    app.edit_nibble(0xa).unwrap();
    app.mode = Mode::Normal;
    app.handle_action(Action::EnterCommand);
    app.handle_action(Action::CommandChar('q'));
    app.handle_action(Action::CommandSubmit);
    assert_eq!(app.mode, Mode::Command);
    assert_eq!(app.command_buffer, "q");
    assert_eq!(app.status_level, crate::app::StatusLevel::Error);
    assert!(app.status_message.contains("unsaved changes"));

    // Success clears buffer and browses history
    let mut app2 = app_with_len(16);
    app2.handle_action(Action::EnterCommand);
    type_command(&mut app2, "goto 0x4");
    app2.handle_action(Action::CommandSubmit);
    app2.handle_action(Action::EnterCommand);
    assert!(app2.command_buffer.is_empty());
    type_command(&mut app2, "goto 0x1");
    app2.handle_action(Action::CommandSubmit);
    app2.handle_action(Action::EnterCommand);
    assert!(app2.command_buffer.is_empty());
    type_command(&mut app2, "und");
    app2.handle_action(Action::CommandHistoryPrev);
    assert_eq!(app2.command_buffer, "goto 0x1");
    app2.handle_action(Action::CommandHistoryPrev);
    assert_eq!(app2.command_buffer, "goto 0x4");
    app2.handle_action(Action::CommandHistoryNext);
    assert_eq!(app2.command_buffer, "goto 0x1");
    app2.handle_action(Action::CommandHistoryNext);
    assert_eq!(app2.command_buffer, "und");
}

fn app_with_nested_inspector() -> App {
    let mut app = app_with_len(8);
    let parent_field = FieldDef {
        name: "parent_byte".to_owned(),
        offset: 0,
        field_type: FieldType::U8,
        description: String::new(),
        editable: true,
    };
    let child_field = FieldDef {
        name: "child_byte".to_owned(),
        offset: 0,
        field_type: FieldType::U8,
        description: String::new(),
        editable: true,
    };
    let structs = vec![StructValue {
        name: "Parent".to_owned(),
        base_offset: 0,
        fields: vec![FieldValue {
            def: parent_field,
            abs_offset: 0,
            raw_bytes: vec![0],
            display: "0x00".to_owned(),
            size: 1,
        }],
        children: vec![StructValue {
            name: "Child".to_owned(),
            base_offset: 4,
            fields: vec![FieldValue {
                def: child_field,
                abs_offset: 4,
                raw_bytes: vec![0],
                display: "0x00".to_owned(),
                size: 1,
            }],
            children: Vec::new(),
        }],
    }];
    let collapsed_nodes = std::collections::BTreeSet::new();
    let rows = crate::format::parse::flatten(&structs, &collapsed_nodes);
    app.show_side_panel = true;
    app.active_side_panel = crate::app::SidePanelKind::Inspector;
    app.inspector_state = Some(crate::app::InspectorState {
        format_name: "TEST".to_owned(),
        structs,
        rows,
        scroll_offset: 0,
        selected_row: 0,
        editing: None,
        collapsed_nodes,
    });
    app.mode = Mode::SidePanel;
    app
}

#[test]
fn inspector_collapse_toggle_and_navigation() {
    let mut app = app_with_nested_inspector();
    // Layout: [0]=Parent header, [1]=parent_byte, [2]=Child header, [3]=child_byte
    assert_eq!(app.inspector().unwrap().rows.len(), 4);
    assert_eq!(app.inspector().unwrap().selected_row, 0); // Parent header

    // Collapse Parent — fields AND child go away.
    app.handle_action(Action::SidePanelToggleCollapse);
    assert_eq!(app.inspector().unwrap().rows.len(), 1);
    assert!(matches!(
        app.inspector().unwrap().rows[0],
        InspectorRow::Header {
            collapsed: true,
            ..
        }
    ));
    assert!(app
        .inspector()
        .unwrap()
        .collapsed_nodes
        .contains(&vec![("Parent".to_owned(), 0)]));

    // Toggle twice restores original rows
    app.handle_action(Action::SidePanelToggleCollapse);
    assert_eq!(app.inspector().unwrap().rows.len(), 4);
    assert!(app.inspector().unwrap().collapsed_nodes.is_empty());

    // Header Enter toggles collapse when not editing
    app.handle_action(Action::SidePanelEnter);
    assert!(app
        .inspector()
        .unwrap()
        .collapsed_nodes
        .contains(&vec![("Parent".to_owned(), 0)]));
    assert_eq!(app.mode, Mode::SidePanel);

    // Navigation stops on collapsible header
    let mut app2 = app_with_nested_inspector();
    assert_eq!(app2.inspector().unwrap().selected_row, 0);
    app2.handle_action(Action::SidePanelDown);
    assert_eq!(app2.inspector().unwrap().selected_row, 1);
    app2.handle_action(Action::SidePanelDown);
    assert_eq!(app2.inspector().unwrap().selected_row, 2);
    assert!(matches!(
        app2.inspector().unwrap().rows[2],
        InspectorRow::Header { .. }
    ));

    // Space during edit inserts into buffer instead of toggling
    let mut app3 = app_with_inspector_field();
    app3.handle_action(Action::SidePanelEnter);
    let before_buf = app3
        .inspector()
        .and_then(|i| i.editing.as_ref())
        .map(|e| e.buffer.clone())
        .unwrap();
    app3.handle_action(Action::SidePanelToggleCollapse);
    let after = app3
        .inspector()
        .and_then(|i| i.editing.as_ref())
        .map(|e| e.buffer.clone())
        .unwrap();
    assert_eq!(after.len(), before_buf.len() + 1);
    assert!(after.ends_with(' '));

    // Toggle on non-header row is noop
    let mut app4 = app_with_nested_inspector();
    app4.handle_action(Action::SidePanelDown);
    assert_eq!(app4.inspector().unwrap().selected_row, 1);
    app4.handle_action(Action::SidePanelToggleCollapse);
    assert!(app4.inspector().unwrap().collapsed_nodes.is_empty());
}
