use super::super::*;
use super::helpers::*;
use crate::core_tui::session::transcript_links::TranscriptLinkTarget;

#[test]
fn transcript_relative_file_reference_is_underlined() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS * 2);
    session.set_workspace_root(Some(vtcode_tui_workspace_root()));

    let decorated = session.decorate_visible_transcript_links(
        vec![transcript_line(format!(
            "See {}",
            transcript_file_fixture_relative_path()
        ))],
        Rect::new(0, 0, 120, 1),
    );

    assert_eq!(session.transcript_file_link_targets.len(), 1);
    let linked_span = decorated[0]
        .spans
        .iter()
        .find(|span| {
            span.content
                .contains(transcript_file_fixture_relative_path())
        })
        .expect("expected linked span");
    assert!(
        linked_span
            .style
            .add_modifier
            .contains(Modifier::UNDERLINED)
    );
}

#[test]
fn hovered_transcript_file_reference_adds_hover_style() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS * 2);
    session.set_workspace_root(Some(vtcode_tui_workspace_root()));

    let line = transcript_line(format!("Open {}", transcript_file_fixture_relative_path()));
    let area = Rect::new(0, 0, 120, 1);
    let _ = session.decorate_visible_transcript_links(vec![line.clone()], area);
    let target = session
        .transcript_file_link_targets
        .first()
        .expect("expected transcript file target")
        .clone();

    assert!(session.update_transcript_file_link_hover(target.area.x, target.area.y));

    let decorated = session.decorate_visible_transcript_links(vec![line], area);
    let linked_span = decorated[0]
        .spans
        .iter()
        .find(|span| {
            span.content
                .contains(transcript_file_fixture_relative_path())
        })
        .expect("expected hovered linked span");
    assert!(
        linked_span
            .style
            .add_modifier
            .contains(Modifier::UNDERLINED)
    );
    assert!(linked_span.style.add_modifier.contains(Modifier::BOLD));
}

#[test]
fn mixed_transcript_file_references_are_all_underlined() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    session.set_workspace_root(Some(vtcode_tui_workspace_root()));
    let temp_file = quoted_transcript_temp_file_path();
    fs::write(&temp_file, "mixed-transcript-link").expect("write mixed transcript temp file");

    let line = transcript_line(format!(
        "Open {} and `{}`",
        transcript_file_fixture_relative_path(),
        temp_file.display()
    ));
    let temp_file_display = temp_file.display().to_string();
    let decorated = session.decorate_visible_transcript_links(vec![line], Rect::new(0, 0, 200, 1));

    assert_eq!(session.transcript_file_link_targets.len(), 2);
    assert!(decorated[0].spans.iter().any(|span| {
        span.content
            .contains(transcript_file_fixture_relative_path())
            && span.style.add_modifier.contains(Modifier::UNDERLINED)
    }));
    assert!(decorated[0].spans.iter().any(|span| {
        span.content.contains(&temp_file_display)
            && span.style.add_modifier.contains(Modifier::UNDERLINED)
    }));

    let _ = fs::remove_file(&temp_file);
}

#[test]
fn plain_click_emits_open_file_event_for_absolute_transcript_path() {
    let mut fixture = SessionWithFileLink::new();
    let (tx, mut rx) = mpsc::unbounded_channel();
    let area = fixture.target_area();

    left_click_session(
        &mut fixture.session,
        &tx,
        area.x,
        area.y,
        KeyModifiers::NONE,
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(InlineEvent::OpenFileInEditor(path)) if path == fixture.path
    ));
}

#[test]
fn repeated_plain_click_on_same_transcript_file_link_is_throttled() {
    let mut fixture = SessionWithFileLink::new();
    let area = fixture.target_area();
    let click = MouseEvent {
        kind: MouseEventKind::Down(MouseButton::Left),
        column: area.x,
        row: area.y,
        modifiers: KeyModifiers::NONE,
    };
    let (tx, mut rx) = mpsc::unbounded_channel();

    left_click_session(
        &mut fixture.session,
        &tx,
        click.column,
        click.row,
        click.modifiers,
    );
    left_click_session(
        &mut fixture.session,
        &tx,
        click.column,
        click.row,
        click.modifiers,
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(InlineEvent::OpenFileInEditor(path)) if path == fixture.path
    ));
    rx.try_recv().unwrap_err();
}

#[test]
fn repeated_plain_click_on_different_transcript_file_links_is_not_throttled() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    let absolute_path = transcript_file_fixture_absolute_path();
    let temp_file = quoted_transcript_temp_file_path();
    fs::write(&temp_file, "transcript-link-throttle").expect("write throttle transcript temp file");
    let quoted_temp_path = format!("`{}`", temp_file.display());
    session.push_line(
        InlineMessageKind::Agent,
        vec![make_segment(&format!(
            "Open {} and {}",
            absolute_path, quoted_temp_path
        ))],
    );

    let _ = visible_transcript(&mut session);
    let first_target = session
        .transcript_file_link_targets
        .iter()
        .find(|target| {
            matches!(
                &target.target,
                TranscriptLinkTarget::File(path) if path.path().display().to_string() == absolute_path
            )
        })
        .expect("expected first transcript file target")
        .clone();
    let second_target = session
        .transcript_file_link_targets
        .iter()
        .find(|target| {
            matches!(
                &target.target,
                TranscriptLinkTarget::File(path) if path.path() == temp_file.as_path()
            )
        })
        .expect("expected second transcript file target")
        .clone();
    let (tx, mut rx) = mpsc::unbounded_channel();

    left_click_session(
        &mut session,
        &tx,
        first_target.area.x,
        first_target.area.y,
        KeyModifiers::NONE,
    );
    left_click_session(
        &mut session,
        &tx,
        second_target.area.x,
        second_target.area.y,
        KeyModifiers::NONE,
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(InlineEvent::OpenFileInEditor(path)) if path == absolute_path
    ));
    assert!(matches!(
        rx.try_recv(),
        Ok(InlineEvent::OpenFileInEditor(path)) if path == temp_file.display().to_string()
    ));

    let _ = fs::remove_file(&temp_file);
}

#[test]
fn double_click_emits_open_file_event_for_absolute_transcript_path() {
    let mut fixture = SessionWithFileLink::new();
    let (tx, mut rx) = mpsc::unbounded_channel();
    let area = fixture.target_area();
    let click = MouseEvent {
        kind: MouseEventKind::Down(MouseButton::Left),
        column: area.x,
        row: area.y,
        modifiers: KeyModifiers::NONE,
    };
    left_click_session(
        &mut fixture.session,
        &tx,
        click.column,
        click.row,
        click.modifiers,
    );
    left_click_session(
        &mut fixture.session,
        &tx,
        click.column,
        click.row,
        click.modifiers,
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(InlineEvent::OpenFileInEditor(path)) if path == fixture.path
    ));
    rx.try_recv().unwrap_err();
}

#[test]
fn modifier_click_emits_open_file_event_for_quoted_path_with_spaces() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    let temp_file = quoted_transcript_temp_file_path();
    fs::write(&temp_file, "transcript-link").expect("write quoted transcript temp file");
    let quoted_path = format!("`{}`", temp_file.display());
    let _ = session.decorate_visible_transcript_links(
        vec![transcript_line(format!("Open {}", quoted_path))],
        Rect::new(0, 0, 200, 1),
    );
    let target = session
        .transcript_file_link_targets
        .iter()
        .find(|target| {
            matches!(
                &target.target,
                TranscriptLinkTarget::File(path) if path.path() == temp_file.as_path()
            )
        })
        .expect("expected quoted transcript file target")
        .clone();
    let (tx, mut rx) = mpsc::unbounded_channel();

    left_click_session(
        &mut session,
        &tx,
        target.area.x,
        target.area.y,
        KeyModifiers::NONE,
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(InlineEvent::OpenFileInEditor(path)) if path == temp_file.display().to_string()
    ));

    let _ = fs::remove_file(&temp_file);
}

#[test]
fn modifier_click_emits_open_url_event_for_explicit_transcript_link() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    let url = "https://example.com/docs".to_string();
    let _ = session.decorate_visible_transcript_links(
        vec![TranscriptLine {
            line: Line::from("Open docs"),
            explicit_links: vec![RenderedTranscriptLink {
                start: 5,
                end: 9,
                start_col: 5,
                width: 4,
                target: InlineLinkTarget::Url(url.clone()),
            }],
        }],
        Rect::new(0, 0, 200, 1),
    );
    let target = session
        .transcript_file_link_targets
        .first()
        .expect("expected transcript url target")
        .clone();
    let (tx, mut rx) = mpsc::unbounded_channel();

    left_click_session(
        &mut session,
        &tx,
        target.area.x,
        target.area.y,
        KeyModifiers::NONE,
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(InlineEvent::OpenUrl(clicked)) if clicked == url
    ));
}

#[test]
fn modifier_click_emits_open_url_event_for_raw_transcript_url() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    let url = "https://auth.openai.com/oauth/authorize?client_id=test".to_string();
    session.push_line(InlineMessageKind::Agent, vec![make_segment(url.as_str())]);
    let _ = rendered_session_lines(&mut session, VIEW_ROWS);
    let target = session
        .transcript_file_link_targets
        .first()
        .expect("expected transcript url target")
        .clone();
    let (tx, mut rx) = mpsc::unbounded_channel();

    left_click_session(
        &mut session,
        &tx,
        target.area.x,
        target.area.y,
        open_file_click_modifiers(),
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(InlineEvent::OpenUrl(clicked)) if clicked == url
    ));
}

#[test]
fn wrapped_transcript_url_last_segment_is_underlined_and_clickable() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    let url = format!(
        "https://auth.openai.com/oauth/authorize?response_type=code&client_id=test&scope=openid%20profile%20email&state={}",
        "abcdefghijklmnopqrstuvwxyz".repeat(12)
    );
    session.push_line(InlineMessageKind::Agent, vec![make_segment(url.as_str())]);

    let transcript_lines = session.reflow_message_lines(0, 60);
    let decorated =
        session.decorate_visible_cached_transcript_links(transcript_lines, Rect::new(0, 0, 60, 8));
    let targets = session
        .transcript_file_link_targets
        .iter()
        .filter(|target| matches!(&target.target, TranscriptLinkTarget::Url(clicked) if clicked == &url))
        .cloned()
        .collect::<Vec<_>>();
    assert!(
        targets.len() >= 2,
        "expected wrapped transcript url segments"
    );

    let target = targets
        .iter()
        .max_by_key(|target| (target.area.y, target.area.x))
        .expect("expected wrapped transcript url target")
        .clone();
    assert!(
        decorated[target.area.y as usize]
            .spans
            .iter()
            .any(|span| span.style.add_modifier.contains(Modifier::UNDERLINED))
    );

    let (tx, mut rx) = mpsc::unbounded_channel();
    left_click_session(
        &mut session,
        &tx,
        target.area.x,
        target.area.y,
        open_file_click_modifiers(),
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(InlineEvent::OpenUrl(clicked)) if clicked == url
    ));
}

#[test]
fn reflowed_tool_lines_include_detected_raw_links() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    let url = "https://example.com/tool-output".to_string();
    session.push_line(
        InlineMessageKind::Tool,
        vec![make_segment(&format!("open {url}"))],
    );

    let transcript_lines = session.reflow_message_lines(0, 80);

    assert!(transcript_lines.iter().any(|line| {
        line.explicit_links
            .iter()
            .any(|link| matches!(&link.target, InlineLinkTarget::Url(target) if target == &url))
    }));
}

#[test]
fn modifier_click_emits_open_url_event_for_modal_auth_link_in_app_session() {
    let mut session = AppSession::new(InlineTheme::default(), None, VIEW_ROWS);
    let url = "https://auth.openai.com/oauth/authorize?client_id=test".to_string();
    session.show_transient(wizard_auth_transient(&url));

    let _ = rendered_app_session_lines(&mut session, 20);
    let target = session
        .core
        .modal_link_targets()
        .first()
        .expect("expected modal url target")
        .clone();
    let (tx, mut rx) = mpsc::unbounded_channel();

    left_click_app_session(
        &mut session,
        &tx,
        target.area.x,
        target.area.y,
        open_file_click_modifiers(),
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(app_types::InlineEvent::OpenUrl(clicked)) if clicked == url
    ));
}

#[test]
fn plain_click_emits_open_url_event_for_wrapped_wizard_modal_auth_link() {
    let mut session = AppSession::new(InlineTheme::default(), None, VIEW_ROWS);
    let url = format!(
        "https://auth.openai.com/oauth/authorize?response_type=code&client_id=test&scope=openid%20profile%20email&state={}",
        "abcdefghijklmnopqrstuvwxyz".repeat(10)
    );
    session.show_transient(wizard_auth_transient(&url));

    let _ = rendered_app_session_lines(&mut session, 20);
    let targets = session
        .core
        .modal_link_targets()
        .iter()
        .filter(|target| matches!(&target.target, TranscriptLinkTarget::Url(clicked) if clicked == &url))
        .cloned()
        .collect::<Vec<_>>();
    assert!(
        !targets.is_empty(),
        "expected visible wizard modal url target"
    );

    let target = targets
        .iter()
        .max_by_key(|target| (target.area.y, target.area.x))
        .expect("expected wrapped wizard modal url target")
        .clone();
    let (tx, mut rx) = mpsc::unbounded_channel();

    left_click_app_session(
        &mut session,
        &tx,
        target.area.x,
        target.area.y,
        KeyModifiers::NONE,
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(app_types::InlineEvent::OpenUrl(clicked)) if clicked == url
    ));
}

#[test]
fn modal_auth_text_in_app_session_is_selectable_and_copied() {
    let _guard = CLIPBOARD_TEST_LOCK
        .lock()
        .expect("clipboard test lock should not be poisoned");

    let mut session = AppSession::new(InlineTheme::default(), None, VIEW_ROWS);
    let url = "https://auth.openai.com/oauth/authorize?client_id=test".to_string();
    session.show_transient(wizard_auth_transient(&url));

    let _ = rendered_app_session_lines(&mut session, 20);
    let target = session
        .core
        .modal_link_targets()
        .first()
        .expect("expected modal url target")
        .clone();
    let (tx, _rx) = mpsc::unbounded_channel::<app_types::InlineEvent>();

    session.handle_event(
        CrosstermEvent::Mouse(MouseEvent {
            kind: MouseEventKind::Down(MouseButton::Left),
            column: target.area.x,
            row: target.area.y,
            modifiers: KeyModifiers::NONE,
        }),
        &tx,
        None,
    );
    session.handle_event(
        CrosstermEvent::Mouse(MouseEvent {
            kind: MouseEventKind::Drag(MouseButton::Left),
            column: target.area.x + 5,
            row: target.area.y,
            modifiers: KeyModifiers::NONE,
        }),
        &tx,
        None,
    );
    session.handle_event(
        CrosstermEvent::Mouse(MouseEvent {
            kind: MouseEventKind::Up(MouseButton::Left),
            column: target.area.x + 5,
            row: target.area.y,
            modifiers: KeyModifiers::NONE,
        }),
        &tx,
        None,
    );

    let backend = TestBackend::new(VIEW_WIDTH, 20);
    let mut terminal = Terminal::new(backend).expect("create test terminal");
    terminal
        .draw(|frame| session.render(frame))
        .expect("render modal selection");

    let buffer = terminal.backend().buffer();
    assert_eq!(
        session
            .core
            .mouse_selection
            .extract_text(buffer, buffer.area),
        "https"
    );
    assert!(session.core.mouse_selection.has_selection);
    assert!(!session.core.mouse_selection.needs_copy());
}

#[test]
fn plain_click_emits_open_url_event_for_standard_modal_auth_link() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    let url = format!(
        "https://auth.openai.com/oauth/authorize?client_id=test&state={}",
        "abcdefghijklmnopqrstuvwxyz".repeat(10)
    );
    session.handle_command(InlineCommand::ShowOverlay {
        request: Box::new(list_auth_overlay(&url)),
    });

    let _ = rendered_session_lines(&mut session, VIEW_ROWS);
    let targets = session
        .modal_link_targets()
        .iter()
        .filter(|target| matches!(&target.target, TranscriptLinkTarget::Url(clicked) if clicked == &url))
        .cloned()
        .collect::<Vec<_>>();
    assert!(!targets.is_empty(), "expected visible modal url target");
    let target = targets.last().expect("expected modal url target").clone();
    let (tx, mut rx) = mpsc::unbounded_channel();

    left_click_session(
        &mut session,
        &tx,
        target.area.x,
        target.area.y,
        KeyModifiers::NONE,
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(InlineEvent::OpenUrl(clicked)) if clicked == url
    ));
}

#[test]
fn repeated_plain_click_on_same_modal_url_link_is_throttled() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    let url = format!(
        "https://auth.openai.com/oauth/authorize?client_id=test&state={}",
        "abcdefghijklmnopqrstuvwxyz".repeat(10)
    );
    session.handle_command(InlineCommand::ShowOverlay {
        request: Box::new(list_auth_overlay(&url)),
    });

    let _ = rendered_session_lines(&mut session, VIEW_ROWS);
    let target = session
        .modal_link_targets()
        .iter()
        .find(|target| matches!(&target.target, TranscriptLinkTarget::Url(clicked) if clicked == &url))
        .expect("expected modal url target")
        .clone();
    let click = MouseEvent {
        kind: MouseEventKind::Down(MouseButton::Left),
        column: target.area.x,
        row: target.area.y,
        modifiers: KeyModifiers::NONE,
    };
    let (tx, mut rx) = mpsc::unbounded_channel();

    left_click_session(&mut session, &tx, click.column, click.row, click.modifiers);
    left_click_session(&mut session, &tx, click.column, click.row, click.modifiers);

    assert!(matches!(
        rx.try_recv(),
        Ok(InlineEvent::OpenUrl(clicked)) if clicked == url
    ));
    rx.try_recv().unwrap_err();
}

#[test]
fn double_click_emits_open_url_event_for_standard_modal_auth_link() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    let url = format!(
        "https://auth.openai.com/oauth/authorize?client_id=test&state={}",
        "abcdefghijklmnopqrstuvwxyz".repeat(10)
    );
    session.handle_command(InlineCommand::ShowOverlay {
        request: Box::new(list_auth_overlay(&url)),
    });

    let _ = rendered_session_lines(&mut session, VIEW_ROWS);
    let target = session
        .modal_link_targets()
        .iter()
        .find(|target| matches!(&target.target, TranscriptLinkTarget::Url(clicked) if clicked == &url))
        .expect("expected modal url target")
        .clone();
    let (tx, mut rx) = mpsc::unbounded_channel();
    let click = MouseEvent {
        kind: MouseEventKind::Down(MouseButton::Left),
        column: target.area.x,
        row: target.area.y,
        modifiers: KeyModifiers::NONE,
    };
    left_click_session(&mut session, &tx, click.column, click.row, click.modifiers);
    left_click_session(&mut session, &tx, click.column, click.row, click.modifiers);

    assert!(matches!(
        rx.try_recv(),
        Ok(InlineEvent::OpenUrl(clicked)) if clicked == url
    ));
    rx.try_recv().unwrap_err();
}

#[test]
fn modifier_click_emits_open_file_event_for_standard_modal_file_link_with_location() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    let absolute_path = transcript_file_fixture_absolute_path();
    let file_target = format!("{absolute_path}#L12C4");
    let canonical_target = format!("{absolute_path}:12:4");
    session.handle_command(InlineCommand::ShowOverlay {
        request: Box::new(OverlayRequest::List(ListOverlayRequest {
            title: "Open source file".to_string(),
            lines: vec![format!("Review this file:\n{file_target}")],
            footer_hint: None,
            items: vec![InlineListItem {
                title: "Continue".to_string(),
                subtitle: None,
                badge: None,
                indent: 0,
                selection: Some(InlineListSelection::SlashCommand("continue".to_string())),
                search_value: None,
            }],
            selected: Some(InlineListSelection::SlashCommand("continue".to_string())),
            search: None,
            hotkeys: Vec::new(),
        })),
    });

    let _ = rendered_session_lines(&mut session, VIEW_ROWS);
    let target = session
        .modal_link_targets()
        .iter()
        .find(|target| {
            matches!(
                &target.target,
                TranscriptLinkTarget::File(path)
                    if path.path().display().to_string() == absolute_path
                        && path.location_suffix() == Some(":12:4")
            )
        })
        .expect("expected modal file target")
        .clone();
    let (tx, mut rx) = mpsc::unbounded_channel();

    left_click_session(
        &mut session,
        &tx,
        target.area.x,
        target.area.y,
        open_file_click_modifiers(),
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(InlineEvent::OpenFileInEditor(path)) if path == canonical_target
    ));
}

#[test]
fn modifier_click_emits_open_file_event_for_explicit_transcript_file_link() {
    let mut session = Session::new(themed_inline_colors(), None, VIEW_ROWS);
    let absolute_path = transcript_file_fixture_absolute_path();
    let _ = session.decorate_visible_transcript_links(
        vec![TranscriptLine {
            line: Line::from("Open file"),
            explicit_links: vec![RenderedTranscriptLink {
                start: 5,
                end: 9,
                start_col: 5,
                width: 4,
                target: InlineLinkTarget::Url(absolute_path.clone()),
            }],
        }],
        Rect::new(0, 0, 200, 1),
    );
    let target = session
        .transcript_file_link_targets
        .first()
        .expect("expected transcript file target")
        .clone();
    let (tx, mut rx) = mpsc::unbounded_channel();

    left_click_session(
        &mut session,
        &tx,
        target.area.x,
        target.area.y,
        open_file_click_modifiers(),
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(InlineEvent::OpenFileInEditor(path)) if path == absolute_path
    ));
}

#[test]
fn explicit_transcript_file_link_uses_theme_accent_color() {
    let mut session = Session::new(themed_inline_colors(), None, VIEW_ROWS);
    let absolute_path = transcript_file_fixture_absolute_path();
    let decorated = session.decorate_visible_transcript_links(
        vec![TranscriptLine {
            line: Line::from("Open file"),
            explicit_links: vec![RenderedTranscriptLink {
                start: 5,
                end: 9,
                start_col: 5,
                width: 4,
                target: InlineLinkTarget::Url(absolute_path),
            }],
        }],
        Rect::new(0, 0, 200, 1),
    );

    let linked_span = decorated[0]
        .spans
        .iter()
        .find(|span| span.content == "file")
        .expect("expected explicit linked span");

    assert_eq!(
        linked_span.style.fg,
        themed_inline_colors()
            .tool_accent
            .map(ratatui_color_from_ansi)
    );
    assert!(
        linked_span
            .style
            .add_modifier
            .contains(Modifier::UNDERLINED)
    );
}

#[test]
fn meta_click_emits_open_file_event_for_transcript_path() {
    let mut fixture = SessionWithFileLink::new();
    let (tx, mut rx) = mpsc::unbounded_channel();
    let area = fixture.target_area();

    // Some terminals (Ghostty, iTerm2) report Cmd as META instead of SUPER
    left_click_session(
        &mut fixture.session,
        &tx,
        area.x,
        area.y,
        KeyModifiers::META,
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(InlineEvent::OpenFileInEditor(path)) if path == fixture.path
    ));
}

#[test]
fn ctrl_click_does_not_emit_open_file_event_on_macos() {
    let mut fixture = SessionWithFileLink::new();
    let (tx, mut rx) = mpsc::unbounded_channel();
    let area = fixture.target_area();

    fixture.session.handle_event(
        CrosstermEvent::Mouse(MouseEvent {
            kind: MouseEventKind::Down(MouseButton::Left),
            column: area.x,
            row: area.y,
            modifiers: KeyModifiers::CONTROL,
        }),
        &tx,
        None,
    );

    rx.try_recv().unwrap_err();
    assert_eq!(fixture.session.mouse_drag_target, MouseDragTarget::None);
    assert!(!fixture.session.mouse_selection.is_selecting);
    assert!(!fixture.session.mouse_selection.has_selection);
}

#[test]
fn command_key_press_then_plain_click_emits_open_file_event_on_macos() {
    let mut fixture = SessionWithFileLink::new();
    let (tx, mut rx) = mpsc::unbounded_channel();
    let area = fixture.target_area();

    fixture.session.handle_event(
        CrosstermEvent::Key(command_modifier_press_event()),
        &tx,
        None,
    );
    left_click_session(
        &mut fixture.session,
        &tx,
        area.x,
        area.y,
        KeyModifiers::NONE,
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(InlineEvent::OpenFileInEditor(path)) if path == fixture.path
    ));

    fixture.session.handle_event(
        CrosstermEvent::Key(command_modifier_release_event()),
        &tx,
        None,
    );
    left_click_session(
        &mut fixture.session,
        &tx,
        area.x,
        area.y,
        KeyModifiers::NONE,
    );

    rx.try_recv().unwrap_err();
}

#[test]
fn meta_key_press_then_plain_click_emits_open_file_event_on_macos() {
    let mut fixture = SessionWithFileLink::new();
    let (tx, mut rx) = mpsc::unbounded_channel();
    let area = fixture.target_area();

    fixture
        .session
        .handle_event(CrosstermEvent::Key(meta_modifier_press_event()), &tx, None);
    left_click_session(
        &mut fixture.session,
        &tx,
        area.x,
        area.y,
        KeyModifiers::NONE,
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(InlineEvent::OpenFileInEditor(path)) if path == fixture.path
    ));
}

#[test]
fn app_session_modifier_click_emits_open_file_event_for_transcript_path() {
    let mut fixture = AppSessionWithFileLink::new();
    let (tx, mut rx) = mpsc::unbounded_channel();
    let area = fixture.target_area();

    left_click_app_session(
        &mut fixture.session,
        &tx,
        area.x,
        area.y,
        open_file_click_modifiers(),
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(app_types::InlineEvent::OpenFileInEditor(path)) if path == fixture.path
    ));
    assert_eq!(
        fixture.session.core.mouse_drag_target,
        MouseDragTarget::None
    );
    assert!(!fixture.session.core.mouse_selection.is_selecting);
    assert!(!fixture.session.core.mouse_selection.has_selection);
}

#[test]
fn app_session_double_click_emits_open_file_event_for_transcript_path() {
    let mut fixture = AppSessionWithFileLink::new();
    let (tx, mut rx) = mpsc::unbounded_channel();
    let area = fixture.target_area();
    let click = MouseEvent {
        kind: MouseEventKind::Down(MouseButton::Left),
        column: area.x,
        row: area.y,
        modifiers: KeyModifiers::NONE,
    };
    left_click_app_session(
        &mut fixture.session,
        &tx,
        click.column,
        click.row,
        click.modifiers,
    );
    left_click_app_session(
        &mut fixture.session,
        &tx,
        click.column,
        click.row,
        click.modifiers,
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(app_types::InlineEvent::OpenFileInEditor(path)) if path == fixture.path
    ));
    rx.try_recv().unwrap_err();
}

#[test]
fn app_session_double_click_emits_open_url_event_for_modal_auth_link() {
    let mut session = AppSession::new(InlineTheme::default(), None, VIEW_ROWS);
    let url = "https://auth.openai.com/oauth/authorize?client_id=test".to_string();
    session.show_transient(wizard_auth_transient(&url));

    let _ = rendered_app_session_lines(&mut session, 20);
    let target = session
        .core
        .modal_link_targets()
        .first()
        .expect("expected modal url target")
        .clone();
    let (tx, mut rx) = mpsc::unbounded_channel();
    let click = MouseEvent {
        kind: MouseEventKind::Down(MouseButton::Left),
        column: target.area.x,
        row: target.area.y,
        modifiers: KeyModifiers::NONE,
    };
    left_click_app_session(&mut session, &tx, click.column, click.row, click.modifiers);
    left_click_app_session(&mut session, &tx, click.column, click.row, click.modifiers);

    assert!(matches!(
        rx.try_recv(),
        Ok(app_types::InlineEvent::OpenUrl(clicked)) if clicked == url
    ));
    rx.try_recv().unwrap_err();
}

#[test]
fn app_session_command_key_press_then_plain_click_emits_open_file_event_on_macos() {
    let mut fixture = AppSessionWithFileLink::new();
    let (tx, mut rx) = mpsc::unbounded_channel();
    let area = fixture.target_area();

    fixture.session.handle_event(
        CrosstermEvent::Key(command_modifier_press_event()),
        &tx,
        None,
    );
    left_click_app_session(
        &mut fixture.session,
        &tx,
        area.x,
        area.y,
        KeyModifiers::NONE,
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(app_types::InlineEvent::OpenFileInEditor(path)) if path == fixture.path
    ));
}

#[test]
fn app_session_ctrl_click_on_link_is_consumed_without_selection() {
    let mut fixture = AppSessionWithFileLink::new();
    let (tx, mut rx) = mpsc::unbounded_channel();
    let area = fixture.target_area();

    fixture.session.handle_event(
        CrosstermEvent::Mouse(MouseEvent {
            kind: MouseEventKind::Down(MouseButton::Left),
            column: area.x,
            row: area.y,
            modifiers: KeyModifiers::CONTROL,
        }),
        &tx,
        None,
    );

    rx.try_recv().unwrap_err();
    assert_eq!(
        fixture.session.core.mouse_drag_target,
        MouseDragTarget::None
    );
    assert!(!fixture.session.core.mouse_selection.is_selecting);
    assert!(!fixture.session.core.mouse_selection.has_selection);
}

#[test]
fn scroll_between_clicks_clears_double_click_history() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    session.push_line(InlineMessageKind::Agent, vec![make_segment("hello world")]);

    let (transcript_area, rendered) = rendered_transcript_lines(&mut session, VIEW_ROWS * 2);
    let row = rendered
        .iter()
        .position(|line| line.contains("hello world"))
        .expect("expected hello world to be rendered");
    let column = rendered[row]
        .find("hello")
        .expect("expected hello word in rendered line") as u16
        + transcript_area.x
        + 1;
    let row = transcript_area.y + row as u16;

    let (tx, _rx) = mpsc::unbounded_channel();
    let click = MouseEvent {
        kind: MouseEventKind::Down(MouseButton::Left),
        column,
        row,
        modifiers: KeyModifiers::NONE,
    };
    let release = MouseEvent {
        kind: MouseEventKind::Up(MouseButton::Left),
        column,
        row,
        modifiers: KeyModifiers::NONE,
    };
    let scroll = MouseEvent {
        kind: MouseEventKind::ScrollDown,
        column,
        row,
        modifiers: KeyModifiers::NONE,
    };

    session.handle_event(CrosstermEvent::Mouse(click), &tx, None);
    session.handle_event(CrosstermEvent::Mouse(release), &tx, None);
    session.handle_event(CrosstermEvent::Mouse(scroll), &tx, None);
    session.handle_event(CrosstermEvent::Mouse(click), &tx, None);
    session.handle_event(CrosstermEvent::Mouse(release), &tx, None);

    assert!(!session.mouse_selection.has_selection);
}

#[test]
fn path_with_line_col_suffix_resolves_correctly() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    let absolute_path = transcript_file_fixture_absolute_path();
    let path_with_loc = format!("{}:42:10", absolute_path);
    session.push_line(
        InlineMessageKind::Agent,
        vec![make_segment(&format!("Error at {}", path_with_loc))],
    );

    let decorated = session.decorate_visible_transcript_links(
        vec![transcript_line(format!("Error at {}", path_with_loc))],
        Rect::new(0, 0, 200, 1),
    );

    assert!(!session.transcript_file_link_targets.is_empty());
    let target = session.transcript_file_link_targets[0].clone();
    assert!(matches!(
        &target.target,
        TranscriptLinkTarget::File(path)
            if path.path().display().to_string() == absolute_path
                && path.location_suffix() == Some(":42:10")
    ));
    let (tx, mut rx) = mpsc::unbounded_channel();
    left_click_session(
        &mut session,
        &tx,
        target.area.x,
        target.area.y,
        open_file_click_modifiers(),
    );
    assert!(matches!(
        rx.try_recv(),
        Ok(InlineEvent::OpenFileInEditor(path)) if path == path_with_loc
    ));
    assert!(
        decorated[0]
            .spans
            .iter()
            .any(|span| { span.style.add_modifier.contains(Modifier::UNDERLINED) })
    );
}

#[test]
fn path_with_paren_location_resolves_correctly() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    let absolute_path = transcript_file_fixture_absolute_path();
    let path_with_loc = format!("{}(10,5)", absolute_path);

    let _ = session.decorate_visible_transcript_links(
        vec![transcript_line(format!("Error at {}", path_with_loc))],
        Rect::new(0, 0, 200, 1),
    );

    assert!(!session.transcript_file_link_targets.is_empty());
    assert!(matches!(
        &session.transcript_file_link_targets[0].target,
        TranscriptLinkTarget::File(path)
            if path.path().display().to_string() == absolute_path
                && path.location_suffix() == Some(":10:5")
    ));
}

#[test]
fn path_with_hash_location_resolves_and_opens_with_canonical_suffix() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    let absolute_path = transcript_file_fixture_absolute_path();
    let hash_path = format!("{absolute_path}#L12C4");

    let _ = session.decorate_visible_transcript_links(
        vec![transcript_line(format!("Error at {}", hash_path))],
        Rect::new(0, 0, 200, 1),
    );

    let target = session
        .transcript_file_link_targets
        .first()
        .expect("expected transcript file target")
        .clone();
    assert!(matches!(
        &target.target,
        TranscriptLinkTarget::File(path)
            if path.path().display().to_string() == absolute_path
                && path.location_suffix() == Some(":12:4")
    ));

    let (tx, mut rx) = mpsc::unbounded_channel();
    left_click_session(
        &mut session,
        &tx,
        target.area.x,
        target.area.y,
        open_file_click_modifiers(),
    );

    assert!(matches!(
        rx.try_recv(),
        Ok(InlineEvent::OpenFileInEditor(path)) if path == format!("{absolute_path}:12:4")
    ));
}

#[test]
fn abbreviation_tokens_are_not_detected_as_paths() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);

    let _ = session.decorate_visible_transcript_links(
        vec![transcript_line("e.g. this or i.e. that")],
        Rect::new(0, 0, 200, 1),
    );

    assert!(session.transcript_file_link_targets.is_empty());
}

#[test]
fn collapsed_paste_review_reflow_includes_detected_raw_links() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    let url = "https://example.com/collapsed-review".to_string();

    let mut json = String::from("{\n");
    for idx in 0..ui::INLINE_JSON_COLLAPSE_LINE_THRESHOLD {
        json.push_str(&format!("  \"key{idx}\": \"value{idx}\",\n"));
    }
    json.push_str(&format!("  \"link\": \"{url}\",\n"));
    json.push_str("  \"end\": true\n}");

    session.append_pasted_message(InlineMessageKind::Tool, json.clone(), json.lines().count());

    let collapsed_index = session.collapsed_pastes[0].line_index;
    let review_lines = session.reflow_message_lines_for_review(collapsed_index, 80);

    assert!(review_lines.iter().any(|line| {
        line.explicit_links
            .iter()
            .any(|link| matches!(&link.target, InlineLinkTarget::Url(target) if target == &url))
    }));
}

#[test]
fn input_compact_preview_for_image_path_with_text() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    let input = "/tmp/Screenshot 2026-02-06 at 3.39.48 PM.png can you see";

    session.insert_paste_text(input);

    let data = session.build_input_widget_data(VIEW_WIDTH, VIEW_ROWS);
    let rendered = text_content(&data.text);
    assert!(rendered.contains("[Image:"));
    assert!(rendered.contains("Screenshot 2026-02-06"));
    assert!(rendered.contains("can you see"));
}

#[test]
fn mouse_wheel_navigates_slash_palette_instead_of_transcript() {
    let mut session = session_with_slash_palette_commands();
    let _ = session.process_key(KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE));

    let _ = rendered_app_session_lines(&mut session, 20);
    let selected_before = session.slash_palette.selected_index();

    let (event_tx, _event_rx) = mpsc::unbounded_channel();
    let panel_area = session.core.bottom_panel_area().expect("panel area");
    session.handle_event(
        CrosstermEvent::Mouse(MouseEvent {
            kind: MouseEventKind::ScrollDown,
            column: panel_area.x,
            row: panel_area.y,
            modifiers: KeyModifiers::NONE,
        }),
        &event_tx,
        None,
    );

    assert_ne!(session.slash_palette.selected_index(), selected_before);
    assert_eq!(session.core.transcript_view_top, 0);
}

#[test]
fn mouse_wheel_navigates_modal_list_instead_of_transcript() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    show_basic_list_overlay(&mut session);
    let _ = rendered_session_lines(&mut session, 20);

    let selected_before = session
        .modal_state()
        .and_then(|modal| modal.list.as_ref())
        .and_then(|list| list.current_selection());

    let (event_tx, _event_rx) = mpsc::unbounded_channel();
    let modal_area = session.modal_list_area.expect("modal list area");
    session.handle_event(
        CrosstermEvent::Mouse(MouseEvent {
            kind: MouseEventKind::ScrollDown,
            column: modal_area.x,
            row: modal_area.y,
            modifiers: KeyModifiers::NONE,
        }),
        &event_tx,
        None,
    );

    let selected_after = session
        .modal_state()
        .and_then(|modal| modal.list.as_ref())
        .and_then(|list| list.current_selection());

    assert_ne!(selected_after, selected_before);
    assert_eq!(session.transcript_view_top, 0);
}

#[test]
fn clicking_selected_slash_row_applies_command() {
    let mut session = session_with_slash_palette_commands();
    for key in [
        KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE),
        KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE),
        KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE),
        KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE),
    ] {
        let _ = session.process_key(key);
    }

    let _ = rendered_app_session_lines(&mut session, 20);
    let panel_area = session.core.bottom_panel_area().expect("panel area");
    let (event_tx, _event_rx) = mpsc::unbounded_channel();
    session.handle_event(
        CrosstermEvent::Mouse(MouseEvent {
            kind: MouseEventKind::Down(MouseButton::Left),
            column: panel_area.x,
            row: panel_area.y + 4,
            modifiers: KeyModifiers::NONE,
        }),
        &event_tx,
        None,
    );

    assert_eq!(session.core.input_manager.content(), "/review ");
    assert!(session.slash_palette.suggestions().is_empty());
}

#[test]
fn clicking_selected_file_palette_row_inserts_reference() {
    let mut session = AppSession::new(InlineTheme::default(), None, VIEW_ROWS);
    let workspace = vtcode_tui_workspace_root();
    load_app_file_palette(
        &mut session,
        vec![
            workspace
                .join("src/core_tui/session.rs")
                .display()
                .to_string(),
        ],
        workspace.clone(),
    );
    session.handle_command(app_types::InlineCommand::SetInput("@".to_string()));

    let _ = rendered_app_session_lines(&mut session, 20);
    let panel_area = session.core.bottom_panel_area().expect("panel area");
    let (event_tx, _event_rx) = mpsc::unbounded_channel();
    session.handle_event(
        CrosstermEvent::Mouse(MouseEvent {
            kind: MouseEventKind::Down(MouseButton::Left),
            column: panel_area.x,
            row: panel_area.y + 5,
            modifiers: KeyModifiers::NONE,
        }),
        &event_tx,
        None,
    );

    assert_eq!(
        session.core.input_manager.content(),
        "@src/core_tui/session.rs "
    );
    assert!(!session.file_palette_active);
}

#[test]
fn clicking_selected_history_row_accepts_entry() {
    let mut session = AppSession::new(InlineTheme::default(), None, VIEW_ROWS);
    session.task_panel_lines = vec!["Active task".to_string()];
    session.set_task_panel_visible(true);
    session.core.set_input("cargo test".to_string());
    let _ = session.process_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
    session.core.set_input("git status".to_string());
    let _ = session.process_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));

    let _ = session.process_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
    let _ = rendered_app_session_lines(&mut session, 20);
    let expected = session
        .history_picker_state
        .selected_match()
        .map(|item| item.content.clone())
        .expect("selected history entry");
    let panel_area = session.core.bottom_panel_area().expect("panel area");
    let (event_tx, _event_rx) = mpsc::unbounded_channel();
    session.handle_event(
        CrosstermEvent::Mouse(MouseEvent {
            kind: MouseEventKind::Down(MouseButton::Left),
            column: panel_area.x,
            row: panel_area.y + 3,
            modifiers: KeyModifiers::NONE,
        }),
        &event_tx,
        None,
    );

    assert!(!session.history_picker_state.active);
    assert_eq!(session.core.input_manager.content(), expected);
    let lines = rendered_app_session_lines(&mut session, 20);
    assert!(
        lines.iter().any(|line| line.contains("Active task")),
        "task panel should resume after the history picker closes"
    );
}

#[test]
fn clicking_input_moves_cursor_to_clicked_position() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    session.set_input("hello world".to_string());
    session.set_cursor(session.input_manager.content().len());

    let _ = rendered_session_lines(&mut session, 20);
    let input_area = session.input_area.expect("input area");
    let (event_tx, _event_rx) = mpsc::unbounded_channel();
    session.handle_event(
        CrosstermEvent::Mouse(MouseEvent {
            kind: MouseEventKind::Down(MouseButton::Left),
            column: input_area.x + 5,
            row: input_area.y,
            modifiers: KeyModifiers::NONE,
        }),
        &event_tx,
        None,
    );

    assert_eq!(session.input_manager.cursor(), 5);
}

#[test]
fn clicking_input_does_not_start_transcript_selection() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    session.set_input("hello world".to_string());
    let _ = rendered_session_lines(&mut session, 20);

    let input_area = session.input_area.expect("input area");
    let (event_tx, _event_rx) = mpsc::unbounded_channel();
    session.handle_event(
        CrosstermEvent::Mouse(MouseEvent {
            kind: MouseEventKind::Down(MouseButton::Left),
            column: input_area.x + 3,
            row: input_area.y,
            modifiers: KeyModifiers::NONE,
        }),
        &event_tx,
        None,
    );

    assert_eq!(session.mouse_drag_target, MouseDragTarget::Input);
    assert!(!session.mouse_selection.is_selecting);
    assert!(!session.mouse_selection.has_selection);
}

#[test]
fn dragging_in_input_creates_input_selection_without_transcript_selection() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    session.set_input("hello world".to_string());
    session.set_cursor(0);
    let _ = rendered_session_lines(&mut session, 20);

    let input_area = session.input_area.expect("input area");
    let (event_tx, _event_rx) = mpsc::unbounded_channel();
    session.handle_event(
        CrosstermEvent::Mouse(MouseEvent {
            kind: MouseEventKind::Down(MouseButton::Left),
            column: input_area.x + 1,
            row: input_area.y,
            modifiers: KeyModifiers::NONE,
        }),
        &event_tx,
        None,
    );
    session.handle_event(
        CrosstermEvent::Mouse(MouseEvent {
            kind: MouseEventKind::Drag(MouseButton::Left),
            column: input_area.x + 6,
            row: input_area.y,
            modifiers: KeyModifiers::NONE,
        }),
        &event_tx,
        None,
    );
    session.handle_event(
        CrosstermEvent::Mouse(MouseEvent {
            kind: MouseEventKind::Up(MouseButton::Left),
            column: input_area.x + 6,
            row: input_area.y,
            modifiers: KeyModifiers::NONE,
        }),
        &event_tx,
        None,
    );

    assert_eq!(session.input_manager.cursor(), 6);
    assert_eq!(session.input_manager.selection_range(), Some((1, 6)));
    assert_eq!(session.mouse_drag_target, MouseDragTarget::None);
    assert!(!session.mouse_selection.is_selecting);
    assert!(!session.mouse_selection.has_selection);
}

#[test]
fn transcript_shows_content_when_viewport_smaller_than_padding() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);

    for index in 0..10 {
        let label = format!("{LABEL_PREFIX}-{index}");
        session.push_line(InlineMessageKind::Agent, vec![make_segment(label.as_str())]);
    }

    let minimal_view_rows = ui::INLINE_HEADER_HEIGHT + Session::input_block_height_for_lines(1) + 1;
    session.force_view_rows(minimal_view_rows);

    let view = visible_transcript(&mut session);
    assert!(
        view.iter()
            .any(|line| line.contains(&format!("{LABEL_PREFIX}-9"))),
        "expected most recent transcript line to remain visible even when viewport is small"
    );
}

#[test]
fn pty_busy_state_does_not_overlay_transcript_status() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    session.active_pty_sessions = Some(Arc::new(AtomicUsize::new(1)));

    let mut visible = vec![TranscriptLine::default(); 2];
    session.overlay_queue_lines(&mut visible, VIEW_WIDTH);
    let rendered: Vec<String> = visible.iter().map(|line| line_text(&line.line)).collect();

    assert!(
        !rendered.iter().any(|line| line.contains("Running...")),
        "busy PTY state should not inject transcript status overlay"
    );
}
