use crossterm::event::{
    Event, KeyCode as K, KeyEvent, KeyModifiers as M, MouseButton, MouseEvent, MouseEventKind,
};
use ratatui::{backend::TestBackend, layout::Rect, Terminal};
use std::{
    fs,
    path::PathBuf,
    sync::atomic::{AtomicUsize, Ordering},
};
use tb_ide::{
    app::{App, Mode, WindowKind},
    commands::Command,
    help::{layout, normalize, token_at, Catalog, Help, Page},
};
struct Temp(PathBuf);
impl Temp {
    fn new() -> Self {
        static N: AtomicUsize = AtomicUsize::new(0);
        let p = std::env::temp_dir().join(format!(
            "tb-help-{}-{}",
            std::process::id(),
            N.fetch_add(1, Ordering::Relaxed)
        ));
        fs::create_dir_all(&p).unwrap();
        Self(p.canonicalize().unwrap())
    }
    fn app(&self) -> App {
        App::new(&self.0, self.0.join("options"), (100, 30)).unwrap()
    }
}
impl Drop for Temp {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.0);
    }
}
fn key(a: &mut App, k: K, m: M) {
    a.handle(Event::Key(KeyEvent::new(k, m)));
}
fn draw(a: &mut App) -> String {
    let mut t = Terminal::new(TestBackend::new(a.size.0, a.size.1)).unwrap();
    t.draw(|f| a.render(f)).unwrap();
    t.backend()
        .buffer()
        .content
        .chunks(a.size.0 as usize)
        .map(|r| r.iter().map(|c| c.symbol()).collect::<String>())
        .collect::<Vec<_>>()
        .join("\n")
}
fn menu(a: &mut App, c: Command) {
    let (m, i) = a
        .menus()
        .iter()
        .enumerate()
        .find_map(|(m, menu)| {
            menu.items
                .iter()
                .position(|i| i.command == Some(c))
                .map(|i| (m, i))
        })
        .unwrap();
    a.menu = Some((m, i));
    key(a, K::Enter, M::NONE);
    assert_eq!(a.last_command, Some(c));
}
#[test]
fn catalog_links_and_missing_targets_are_checked() {
    let c = Catalog::embedded();
    c.validate().unwrap();
    for p in &c.pages {
        for h in &p.headings {
            let (_, b) = c.resolve("", &format!("{}#{}", p.path, h.anchor)).unwrap();
            assert_eq!(b, h.block);
        }
    }
    let duplicates = Catalog::new(&[("a.md", "# Titel\n\n## Titel\n\n## Titel-1\n\n## Titel\n")]);
    for h in &duplicates.pages[0].headings {
        assert_eq!(
            duplicates
                .resolve("", &format!("a.md#{}", h.anchor))
                .unwrap()
                .1,
            h.block
        );
    }
    let bad = Catalog::new(&[("docs/a.md", "# A\n\n[broken](missing.md)")]);
    assert!(bad.validate().unwrap_err().to_string().contains("fehlt"));
    let bad = Catalog::new(&[("docs/a.md", "# A\n\n[broken](#missing)")]);
    assert!(bad.validate().is_err());
    for target in [
        "../../secret",
        "%2e%2e/%2e%2e/secret",
        "/etc/passwd",
        "file:///etc/passwd",
        "../missing.md",
        "hilfe.md#missing",
        "hilfe.md#%GG",
        "C:%5csecret",
    ] {
        assert!(c.resolve("docs/hilfe.md", target).is_err(), "{target}");
    }
    assert!(c.resolve("docs/hilfe.md", "../PLAN.md").is_ok());
    let t = Temp::new();
    let mut a = t.app();
    a.execute(Command::UsingHelp);
    let before = a.help.position.clone();
    assert!(a
        .help
        .open("https://example.org/private")
        .unwrap_err()
        .to_string()
        .contains("Extern"));
    assert_eq!(a.help.position, before);
}
#[test]
fn markdown_blocks_escapes_links_and_unicode_layout() {
    let p=Page::parse("sample","# Überschrift\n\nAbsatz mit **fett**, *kursiv*, \\*Escape\\* und [Link](#überschrift).\n\n1. Eins\n2. Zwei\n\n```basic\nPRINT \"界ä\"\n012345678901234567890123456789\n```\n\n| Spalte | Breit |\n|---|---|\n| Wert | 界界界界 |\n");
    let text = p
        .blocks
        .iter()
        .map(|b| b.text())
        .collect::<Vec<_>>()
        .join("\n");
    assert!(text.contains("*Escape*"));
    assert!(text.contains("◄Link►"));
    assert!(text.contains("1. Eins"));
    assert!(text.contains("2. Zwei"));
    assert!(p.blocks.iter().any(|b| b.pre && b.text().contains("PRINT")));
    assert!(p.blocks.iter().any(|b| b.pre && b.text().contains("界界")));
    assert_eq!(p.headings[0].anchor, "überschrift");
    for width in [1, 7, 20, 80] {
        let rows = layout(&p, width);
        for (i, b) in p.blocks.iter().enumerate() {
            let reconstructed: String = rows
                .iter()
                .filter(|r| r.block == i)
                .flat_map(|r| b.glyphs[r.start..r.end].iter().map(|g| g.ch))
                .collect();
            assert_eq!(reconstructed, b.text());
        }
    }
}
#[test]
fn offline_help_child() {
    if std::env::var_os("TB_HELP_OFFLINE_CHILD").is_none() {
        return;
    }
    let base = std::env::current_dir().unwrap();
    assert!(!base.join("docs").exists());
    let mut a = App::new(&base, base.join("options"), (100, 30)).unwrap();
    for command in [
        Command::HelpIndex,
        Command::HelpContents,
        Command::Keyboard,
        Command::UsingHelp,
        Command::Tutorial,
    ] {
        a.execute(command);
        assert_eq!(a.active_window().unwrap().kind, WindowKind::Help);
        assert!(!draw(&mut a).is_empty());
    }
    for path in [
        "docs/sprachreferenz.md",
        "docs/bibliothek.md",
        "docs/forms-referenz.md",
        "docs/ide-bedienung.md",
        "docs/tutorial.md",
    ] {
        let (p, b) = Catalog::embedded().resolve("", path).unwrap();
        a.help.visit(p, b, None);
        assert!(a.help.page().blocks.len() > 10);
        assert!(draw(&mut a).contains(&a.help.page().title));
    }
}
#[test]
fn help_works_from_a_directory_without_checkout() {
    let t = Temp::new();
    let status = std::process::Command::new(std::env::current_exe().unwrap())
        .current_dir(&t.0)
        .env("TB_HELP_OFFLINE_CHILD", "1")
        .args(["--exact", "offline_help_child", "--nocapture"])
        .status()
        .unwrap();
    assert!(status.success());
}
#[test]
fn context_functions_properties_commands_and_unknown_queries() {
    assert_eq!(normalize("lEfT$"), "LEFT");
    assert_eq!(token_at("PRINT LEFT$(x$, 2)", 8), "LEFT$");
    let t = Temp::new();
    let mut a = t.app();
    let id = a.active_document().unwrap();
    a.project
        .replace_text(id, 0..0, "PRINT lEfT$(\"abc\", 2)")
        .unwrap();
    let view = match a.active_window().unwrap().kind {
        WindowKind::Code(v) => v,
        _ => panic!(),
    };
    a.project.view_mut(view).unwrap().cursor = 9;
    key(&mut a, K::F(1), M::NONE);
    assert_eq!(a.help.page().path, "docs/bibliothek.md");
    assert!(a.help.page().blocks[a.help.position.block]
        .text()
        .contains("Strings"));
    key(&mut a, K::Esc, M::NONE);
    a.execute(Command::NewForm);
    assert_eq!(a.mode, Mode::Designer);
    let specs = tb_frontend::forms::properties(tb_frontend::forms::ObjectClass::Form);
    a.designer.property = specs.iter().position(|p| p.name == "CAPTION").unwrap();
    a.properties = true;
    let previous = a.active;
    key(&mut a, K::F(2), M::NONE);
    key(&mut a, K::F(1), M::NONE);
    assert_eq!(a.help.page().path, "docs/forms-referenz.md");
    assert!(a.help.page().blocks[a.help.position.block]
        .text()
        .contains("Caption"));
    assert!(draw(&mut a).contains("Caption"));
    key(&mut a, K::Esc, M::NONE);
    assert_eq!(a.active, previous);
    assert!(a.properties && a.value_focus);
    a.value_focus = false;
    a.properties = false;
    let (m, i) = a
        .menus()
        .iter()
        .enumerate()
        .find_map(|(m, menu)| {
            menu.items
                .iter()
                .position(|i| i.command == Some(Command::SaveProject))
                .map(|i| (m, i))
        })
        .unwrap();
    a.menu = Some((m, i));
    key(&mut a, K::F(1), M::NONE);
    assert!(a.help.page().blocks[a.help.position.block]
        .text()
        .contains("Speichern"));
    a.help_context("unbekanntxyz", None);
    assert!(draw(&mut a).contains("unbekanntxyz"));
    assert!(draw(&mut a).contains("Kein passendes Thema"));
    a.help_context("PRINT", None);
    assert!(a.help.position.query.is_some());
    assert!(a.help.page().links.len() > 2);
}
#[test]
fn links_history_resize_scroll_and_focus_use_real_events() {
    let t = Temp::new();
    let mut a = t.app();
    let previous = a.active;
    menu(&mut a, Command::HelpContents);
    let help_id = a.active;
    key(&mut a, K::Tab, M::NONE);
    let first = a.help.position.link;
    key(&mut a, K::Tab, M::NONE);
    assert_ne!(a.help.position.link, first);
    key(&mut a, K::BackTab, M::SHIFT);
    assert_eq!(a.help.position.link, first);
    key(&mut a, K::Down, M::NONE);
    let before = a.help.position.clone();
    key(&mut a, K::Enter, M::NONE);
    assert_ne!(a.help.position.page, before.page);
    key(&mut a, K::F(1), M::ALT);
    assert_eq!(a.help.position, before);
    let w = a.windows.iter_mut().find(|w| w.id == help_id).unwrap();
    w.normal = Rect::new(2, 2, 28, 18);
    let small = draw(&mut a);
    assert!(small.contains("Contents"));
    let pos = a.help.position.clone();
    a.handle(Event::Resize(120, 40));
    a.windows
        .iter_mut()
        .find(|w| w.id == help_id)
        .unwrap()
        .normal
        .width = 85;
    draw(&mut a);
    assert_eq!(a.help.position, pos);
    let (p, b) = Catalog::embedded().resolve("", "docs/tutorial.md").unwrap();
    a.help.visit(p, b, None);
    key(&mut a, K::PageDown, M::NONE);
    assert!(a.help.position.block > 0);
    key(&mut a, K::Home, M::NONE);
    assert_eq!(a.help.position.block, 0);
    key(&mut a, K::Esc, M::NONE);
    assert_eq!(a.active, previous);
    assert!(!a.windows.iter().any(|w| w.kind == WindowKind::Help));
}
#[test]
fn twenty_back_steps_end_of_contents_and_all_entries() {
    let mut h = Help::default();
    let mut states = vec![];
    for i in 0..25 {
        states.push(h.position.clone());
        h.visit(i % 12, i, None);
    }
    assert_eq!(h.history.len(), 20);
    for expected in states.into_iter().rev().take(20) {
        h.back();
        assert_eq!(h.position, expected);
    }
    let p = h.position.clone();
    h.back();
    assert_eq!(h.position, p);
    assert!(h.notice.contains("20"));
    h.visit(11, 0, None);
    let p = h.position.clone();
    h.next();
    assert_eq!(h.position, p);
    assert!(h.notice.contains("Ende"));
    let t = Temp::new();
    let mut a = t.app();
    for (c, path) in [
        (Command::HelpIndex, "index"),
        (Command::HelpContents, "contents"),
        (Command::Keyboard, "docs/tastatur.md"),
        (Command::UsingHelp, "docs/hilfe.md"),
        (Command::Tutorial, "docs/tutorial.md"),
    ] {
        menu(&mut a, c);
        assert_eq!(a.help.page().path, path);
        assert!(draw(&mut a).contains(&a.help.page().title));
    }
    key(&mut a, K::F(1), M::SHIFT);
    assert_eq!(a.help.page().path, "docs/hilfe.md");
    key(&mut a, K::F(1), M::CONTROL);
    assert_eq!(a.help.page().path, "docs/tutorial.md");
    menu(&mut a, Command::About);
    let title = &a.dialog.as_ref().unwrap().title;
    assert!(title.contains(env!("CARGO_PKG_VERSION")));
    assert!(title.contains(env!("CARGO_PKG_AUTHORS")));
    assert!(title.contains("Copyright"));
}
#[test]
fn right_click_configuration_and_external_link_follow() {
    let t = Temp::new();
    let mut a = t.app();
    let event = Event::Mouse(MouseEvent {
        kind: MouseEventKind::Down(MouseButton::Right),
        column: 2,
        row: 3,
        modifiers: M::NONE,
    });
    a.options.right_help = false;
    a.handle(event.clone());
    assert_ne!(a.active_window().unwrap().kind, WindowKind::Help);
    a.options.right_help = true;
    a.handle(event);
    assert_eq!(a.active_window().unwrap().kind, WindowKind::Help);
    let (p, b) = Catalog::embedded()
        .resolve("", "docs/dateiformate.md")
        .unwrap();
    a.help.visit(p, b, None);
    let link = a
        .help
        .page()
        .links
        .iter()
        .position(|l| l.target.starts_with("https:"))
        .unwrap();
    for _ in 0..=link {
        key(&mut a, K::Tab, M::NONE);
    }
    assert!(draw(&mut a).contains("extern"));
    let before = a.help.position.clone();
    key(&mut a, K::Enter, M::NONE);
    assert_eq!(a.help.position, before);
    assert!(a.help.notice.contains("nicht automatisch"));
    assert!(!a.session.file_shell);
    assert!(a.session.host.shell_request.is_none());
}

#[test]
fn every_command_and_forms_alias_resolves_and_invalid_alias_fails() {
    use tb_frontend::forms::ObjectClass;
    let c = Catalog::embedded();
    for designer in [false, true] {
        for menu in tb_ide::commands::menus(designer) {
            for command in menu.items.iter().filter_map(|i| i.command) {
                c.resolve("", tb_ide::help::command_target(command, designer))
                    .unwrap();
            }
        }
    }
    let t = Temp::new();
    let mut a = t.app();
    for class in ObjectClass::ALL {
        for property in tb_frontend::forms::properties(class) {
            a.help_context(property.name, Some(class.name()));
            assert!(
                a.help.position.query.is_none(),
                "{}.{}",
                class.name(),
                property.name
            );
            assert_eq!(a.help.page().path, "docs/forms-referenz.md");
            let page = a.help.page();
            let heading = page
                .headings
                .iter()
                .rev()
                .find(|h| h.block <= a.help.position.block)
                .unwrap();
            assert!(heading.title.to_uppercase().contains(class.name()));
        }
    }
    // The same resolver validates command aliases; a stale anchor is a hard failure.
    assert!(c
        .resolve(
            "",
            "docs/ide-bedienung.md#absichtlich-fehlender-befehlsanker"
        )
        .is_err());
}

#[test]
fn code_and_tables_remain_reachable_after_resize_and_horizontal_scroll() {
    let t = Temp::new();
    let mut a = t.app();
    a.execute(Command::HelpContents);
    a.windows
        .iter_mut()
        .find(|w| w.id == a.active)
        .unwrap()
        .normal = Rect::new(2, 2, 22, 18);
    let (p, _) = Catalog::embedded().resolve("", "docs/tutorial.md").unwrap();
    let code = Catalog::embedded().pages[p]
        .blocks
        .iter()
        .position(|b| b.pre && b.text().contains("Hallo aus"))
        .unwrap();
    a.help.visit(p, code, None);
    let before = a.help.position.clone();
    let small = draw(&mut a);
    assert!(small.contains("PRINT"));
    for _ in 0..3 {
        key(&mut a, K::Right, M::NONE);
    }
    assert!(draw(&mut a).contains("Formular"));
    let after_scroll = a.help.position.clone();
    a.handle(Event::Resize(110, 35));
    a.windows
        .iter_mut()
        .find(|w| w.id == a.active)
        .unwrap()
        .normal
        .width = 70;
    draw(&mut a);
    assert_eq!(a.help.position, after_scroll);
    for _ in 0..3 {
        key(&mut a, K::Left, M::NONE);
    }
    assert_eq!(a.help.position, before);
    assert!(draw(&mut a).contains("Hallo aus dem Formular"));
    let (p, _) = Catalog::embedded()
        .resolve("", "docs/ide-bedienung.md")
        .unwrap();
    let row = Catalog::embedded().pages[p]
        .blocks
        .iter()
        .position(|b| b.pre && b.text().contains("Index, Contents"))
        .unwrap();
    a.help.visit(p, row, None);
    a.windows
        .iter_mut()
        .find(|w| w.id == a.active)
        .unwrap()
        .normal
        .width = 22;
    let initial = draw(&mut a);
    assert!(!initial.contains("Using Help"));
    let mut found = false;
    for _ in 0..20 {
        key(&mut a, K::Right, M::NONE);
        found |= draw(&mut a).contains("Using Help");
    }
    assert!(found);
    // Real Unicode paragraphs wrap and remain at the identical logical anchor.
    a.execute(Command::UsingHelp);
    key(&mut a, K::PageDown, M::NONE);
    let pos = a.help.position.clone();
    let narrow = draw(&mut a);
    assert!(!narrow.contains('\u{fffd}'));
    a.windows
        .iter_mut()
        .find(|w| w.id == a.active)
        .unwrap()
        .normal
        .width = 90;
    draw(&mut a);
    assert_eq!(a.help.position, pos);
}

#[test]
fn help_preserves_fullscreen_and_does_not_modify_designer_or_source() {
    let t = Temp::new();
    let mut a = t.app();
    a.execute(Command::Output);
    a.session.fullscreen = true;
    let original = a.active;
    key(&mut a, K::F(1), M::SHIFT);
    assert!(!a.session.fullscreen);
    assert!(draw(&mut a).contains("Hilfe benutzen"));
    key(&mut a, K::Esc, M::NONE);
    assert_eq!(a.active, original);
    assert!(a.session.fullscreen);
    a.session.fullscreen = false;
    a.execute(Command::NewForm);
    let doc = a.designer.document.unwrap();
    let before = a.project.document(doc).unwrap().revision();
    key(&mut a, K::F(1), M::SHIFT);
    for k in [K::Down, K::Right, K::Tab, K::PageDown, K::Delete] {
        key(&mut a, k, M::NONE);
    }
    assert_eq!(a.project.document(doc).unwrap().revision(), before);
    for ch in "LEFT$".chars() {
        key(&mut a, K::Char(ch), M::NONE);
    }
    key(&mut a, K::Enter, M::NONE);
    assert!(a.help.position.query.as_deref() == Some("LEFT$"));
    key(&mut a, K::Tab, M::NONE);
    key(&mut a, K::Enter, M::NONE);
    assert!(a.help.position.query.is_none());
    key(&mut a, K::F(1), M::ALT);
    assert_eq!(a.help.position.query.as_deref(), Some("LEFT$"));
    assert_eq!(a.project.document(doc).unwrap().revision(), before);
}
