//! Unit + snapshot tests for the Library pane.
//!
//! Snapshot tests use `ratatui::backend::TestBackend` so the run loop
//! is bypassed entirely — we drive `render_library` directly against
//! a synthetic `App`.

use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use kebab_config::Config;
use kebab_core::{
    ChunkerVersion, DocSummary, DocumentId, Lang, ParserVersion, SourceType, TrustLevel,
    WorkspacePath,
};
use kebab_tui::{App, KeyOutcome, Pane, render_library};
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use ratatui::layout::Rect;
use time::OffsetDateTime;

fn make_doc(path: &str, title: &str, tags: Vec<&str>) -> DocSummary {
    DocSummary {
        doc_id: DocumentId(format!(
            "{:0<32}",
            path.chars()
                .filter(|c| c.is_alphanumeric())
                .collect::<String>()
        )),
        doc_path: WorkspacePath::new(path.into()).unwrap(),
        title: title.into(),
        lang: Lang("en".into()),
        tags: tags.into_iter().map(String::from).collect(),
        trust_level: TrustLevel::Primary,
        source_type: SourceType::Note,
        byte_len: 1024,
        chunk_count: 4,
        created_at: OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap(),
        updated_at: OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap(),
        parser_version: ParserVersion("test-parser".into()),
        chunker_version: ChunkerVersion("test-chunker".into()),
    }
}

fn app_with_docs(docs: Vec<DocSummary>) -> App {
    let mut config = Config::defaults();
    // Storage paths point at /tmp so any accidental facade call
    // would not touch the user's real KB. Tests below use the
    // `populate_library_for_testing` test seam, never the facade.
    config.storage.data_dir = "/tmp/kebab-tui-tests-noop".to_string();
    let mut app = App::new(config).expect("App::new must succeed with defaults");
    app.populate_library_for_testing(docs);
    app
}

#[test]
fn empty_library_renders_block_only_no_panic() {
    let app = app_with_docs(vec![]);
    let backend = TestBackend::new(80, 20);
    let mut terminal = Terminal::new(backend).unwrap();
    terminal
        .draw(|f| {
            let area = Rect::new(0, 0, 80, 20);
            render_library(f, area, &app);
        })
        .unwrap();
    let buffer = terminal.backend().buffer().clone();
    let rendered: String = (0..buffer.area.height)
        .map(|y| {
            (0..buffer.area.width)
                .map(|x| buffer[(x, y)].symbol())
                .collect::<String>()
        })
        .collect::<Vec<_>>()
        .join("\n");
    assert!(
        rendered.contains("Library"),
        "rendered frame must show Library header: {rendered}"
    );
    assert!(
        rendered.contains("no docs") || rendered.contains("Library"),
        "empty state hint should appear in the header line"
    );
}

#[test]
fn handle_key_library_q_quits() {
    let mut app = app_with_docs(vec![]);
    let outcome = kebab_tui::handle_key_library(
        &mut app,
        KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE),
    );
    assert_eq!(outcome, KeyOutcome::Quit);
}

#[test]
fn handle_key_library_esc_quits_when_no_overlay() {
    let mut app = app_with_docs(vec![]);
    let outcome =
        kebab_tui::handle_key_library(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
    assert_eq!(outcome, KeyOutcome::Quit);
}

#[test]
fn handle_key_library_slash_switches_to_search() {
    let mut app = app_with_docs(vec![]);
    let outcome = kebab_tui::handle_key_library(
        &mut app,
        KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE),
    );
    assert_eq!(outcome, KeyOutcome::SwitchPane(Pane::Search));
}

#[test]
fn handle_key_library_question_switches_to_ask() {
    let mut app = app_with_docs(vec![]);
    let outcome = kebab_tui::handle_key_library(
        &mut app,
        KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE),
    );
    assert_eq!(outcome, KeyOutcome::SwitchPane(Pane::Ask));
}

#[test]
fn handle_key_library_enter_does_not_switch_when_empty() {
    let mut app = app_with_docs(vec![]);
    let outcome =
        kebab_tui::handle_key_library(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
    assert_eq!(outcome, KeyOutcome::Continue);
}

#[test]
fn library_with_docs_renders_titles() {
    let app = app_with_docs(vec![
        make_doc("notes/foo.md", "Foo", vec!["alpha"]),
        make_doc("notes/bar.md", "Bar", vec!["beta", "gamma"]),
        make_doc("notes/baz.md", "Baz Title", vec![]),
    ]);
    let backend = TestBackend::new(80, 10);
    let mut terminal = Terminal::new(backend).unwrap();
    terminal
        .draw(|f| {
            let area = Rect::new(0, 0, 80, 10);
            render_library(f, area, &app);
        })
        .unwrap();
    let buffer = terminal.backend().buffer().clone();
    let rendered: String = (0..buffer.area.height)
        .map(|y| {
            (0..buffer.area.width)
                .map(|x| buffer[(x, y)].symbol())
                .collect::<String>()
        })
        .collect::<Vec<_>>()
        .join("\n");
    for title in &["Foo", "Bar", "Baz Title"] {
        assert!(
            rendered.contains(title),
            "rendered must contain {title}, got:\n{rendered}"
        );
    }
}

#[test]
fn handle_key_library_arrow_down_moves_selection() {
    let mut app = app_with_docs(vec![
        make_doc("a.md", "A", vec![]),
        make_doc("b.md", "B", vec![]),
        make_doc("c.md", "C", vec![]),
    ]);
    let outcome = kebab_tui::handle_key_library(
        &mut app,
        KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE),
    );
    assert_eq!(outcome, KeyOutcome::Continue);
    let outcome2 = kebab_tui::handle_key_library(
        &mut app,
        KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE),
    );
    assert_eq!(outcome2, KeyOutcome::Continue);
    // Third j hits the bottom; clamp must not panic / overflow.
    let outcome3 = kebab_tui::handle_key_library(
        &mut app,
        KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE),
    );
    assert_eq!(outcome3, KeyOutcome::Continue);
}

#[test]
fn handle_key_library_enter_inspects_when_docs_present() {
    let mut app = app_with_docs(vec![make_doc("a.md", "A", vec![])]);
    let outcome =
        kebab_tui::handle_key_library(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
    assert_eq!(outcome, KeyOutcome::SwitchPane(Pane::Inspect));
}

#[test]
fn handle_key_library_f_opens_filter_overlay_then_enter_refreshes() {
    let mut app = app_with_docs(vec![make_doc("a.md", "A", vec![])]);
    // Open filter.
    let o1 = kebab_tui::handle_key_library(
        &mut app,
        KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE),
    );
    assert_eq!(o1, KeyOutcome::Continue);
    // Type into tags buffer.
    for ch in "foo".chars() {
        kebab_tui::handle_key_library(
            &mut app,
            KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE),
        );
    }
    // Enter commits + refreshes.
    let o2 =
        kebab_tui::handle_key_library(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
    assert_eq!(o2, KeyOutcome::Refresh);
}

/// p9-fb-10: filter overlay accepts Hangul tags via key events
/// and commits them to the doc filter.
#[test]
fn filter_overlay_accepts_hangul_tags() {
    let mut app = app_with_docs(vec![make_doc("a.md", "A", vec![])]);
    // Open filter overlay.
    let o1 = kebab_tui::handle_key_library(
        &mut app,
        KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE),
    );
    assert_eq!(o1, KeyOutcome::Continue);
    // Type Hangul into the tags buffer.
    for ch in "한글".chars() {
        kebab_tui::handle_key_library(
            &mut app,
            KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE),
        );
    }
    // Enter commits.
    let o2 =
        kebab_tui::handle_key_library(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
    assert_eq!(o2, KeyOutcome::Refresh);
    // The library filter should now contain "한글" as a tag.
    let filter = app.library_filter_for_testing();
    assert!(
        filter.tags_any.iter().any(|t| t == "한글"),
        "expected '한글' in tags filter: {:?}",
        filter.tags_any,
    );
}

/// p9-fb-10: filter overlay calls f.set_cursor_position so ratatui
/// shows the caret on the focused field. Pin: after opening the
/// overlay, render → terminal cursor is set + has non-zero x
/// (the label offset > 0).
#[test]
fn filter_overlay_render_places_cursor_on_focused_field() {
    let mut app = app_with_docs(vec![make_doc("a.md", "A", vec![])]);
    // Open filter.
    let _ = kebab_tui::handle_key_library(
        &mut app,
        KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE),
    );
    let backend = TestBackend::new(80, 20);
    let mut terminal = Terminal::new(backend).unwrap();
    terminal
        .draw(|f| {
            let area = Rect::new(0, 0, 80, 20);
            render_library(f, area, &app);
        })
        .expect("render must not panic");
    // After draw, ratatui calls backend.set_cursor_position when the
    // frame's cursor_position is Some. The terminal's
    // get_cursor_position proxies to the backend.
    let pos = terminal
        .get_cursor_position()
        .expect("filter overlay must call set_cursor_position, so cursor pos must be readable");
    // The Tags label ("tags_any (csv): ") has display_width 16; inner.x
    // is 1 (inside border). With empty input cursor_col=0, expected x=17.
    // We assert x>0 to avoid hardcoding the exact layout geometry while
    // still confirming set_cursor_position was called with a meaningful
    // offset (not stuck at origin).
    assert!(
        pos.x > 0,
        "cursor x should be positive (label offset > 0): {pos:?}"
    );
}

/// p9-fb-24: rendered Library pane shows the column header row above
/// the data rows. Header is in `Role::Heading` style; data rows in
/// the `Role::Body` / `Role::Selected` defaults.
#[test]
fn library_renders_column_header_row() {
    let docs = vec![
        make_doc("notes/alpha.md", "doc-alpha", vec!["rust"]),
        make_doc("notes/beta.md", "doc-beta", vec!["docs"]),
        make_doc("notes/gamma.md", "doc-gamma", vec![]),
    ];
    let app = app_with_docs(docs);
    let backend = TestBackend::new(80, 20);
    let mut terminal = Terminal::new(backend).unwrap();
    terminal
        .draw(|f| {
            let area = Rect::new(0, 0, 80, 20);
            render_library(f, area, &app);
        })
        .unwrap();
    let buffer = terminal.backend().buffer().clone();
    let rendered: String = (0..buffer.area.height)
        .map(|y| {
            (0..buffer.area.width)
                .map(|x| buffer[(x, y)].symbol())
                .collect::<String>()
        })
        .collect::<Vec<_>>()
        .join("\n");
    assert!(
        rendered.contains("TITLE")
            && rendered.contains("TAGS")
            && rendered.contains("UPDATED")
            && rendered.contains("CHUNKS"),
        "header row labels not visible in:\n{rendered}"
    );
    let title_line_idx = rendered
        .lines()
        .position(|line| line.contains("TITLE"))
        .expect("TITLE header should be present");
    let lines_after = rendered
        .lines()
        .skip(title_line_idx + 1)
        .collect::<Vec<_>>();
    assert!(
        lines_after.iter().any(|line| line.contains("doc-")),
        "no data rows after header:\n{rendered}"
    );
}

/// p9-fb-10: Library renders Hangul / CJK titles without overflowing
/// the title column. Smoke pin — render with a mixed Korean fixture
/// and confirm no panic + the truncated width fits the column.
#[test]
fn library_renders_korean_titles_without_overflow() {
    let docs = vec![
        make_doc(
            "ko/한글-노트.md",
            "러스트로 만드는 지식 베이스",
            vec!["rust", "한글"],
        ),
        make_doc("jp/漢字メモ.md", "日本語のテストドキュメント", vec!["jp"]),
        make_doc("mix/hello-세계.md", "Hello, 세계 mixed title", vec!["mix"]),
    ];
    let app = app_with_docs(docs);
    let backend = TestBackend::new(80, 20);
    let mut terminal = Terminal::new(backend).unwrap();
    terminal
        .draw(|f| {
            let area = Rect::new(0, 0, 80, 20);
            render_library(f, area, &app);
        })
        .expect("render must not panic on CJK titles");
    let buffer = terminal.backend().buffer().clone();
    let rendered: String = (0..buffer.area.height)
        .map(|y| {
            (0..buffer.area.width)
                .map(|x| buffer[(x, y)].symbol())
                .collect::<String>()
        })
        .collect::<Vec<_>>()
        .join("\n");
    // At least one Hangul / Kanji glyph survives the render path.
    // TestBackend renders wide chars one-per-cell with the trailing
    // cell empty, so the joined string has spaces between adjacent
    // wide chars — assert single glyphs, not multi-char substrings.
    assert!(
        rendered.contains('러') || rendered.contains('한'),
        "expected a Hangul glyph in rendered frame: {rendered}"
    );
    assert!(
        rendered.contains('日') || rendered.contains('漢'),
        "expected a Kanji glyph in rendered frame: {rendered}"
    );
}
