use std::time::Duration;
use std::time::Instant;

use crossterm::event::KeyCode;
use ratatui::style::Color;

use super::toast::ToastDismissal;
use super::toast::ToastLifetime;
use super::toast::ToastPhase;
use super::toast::ToastTaskStatus;
use super::*;
use crate::FocusedPane;
use crate::Framework;
use crate::KeyBind;
use crate::KeyOutcome;
use crate::NoToastAction;

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum TestPaneId {
    Main,
}

struct TestApp {
    framework: Framework<Self>,
}

impl AppContext for TestApp {
    type AppPaneId = TestPaneId;
    type ToastAction = NoToastAction;

    fn framework(&self) -> &Framework<Self> { &self.framework }
    fn framework_mut(&mut self) -> &mut Framework<Self> { &mut self.framework }
}

fn toasts() -> Toasts<TestApp> { Toasts::new() }

#[test]
fn timed_toast_expires_at_timeout() {
    let mut toasts = toasts();
    let id = toasts.push_timed("done", "body", Duration::ZERO, 1);

    toasts.prune(Instant::now());

    assert!(!toasts.is_alive(id));
}

#[test]
fn persistent_toast_survives_prune() {
    let mut toasts = toasts();
    let id = toasts.push_persistent("error", "body", ToastStyle::Error, None, 1);

    toasts.prune(Instant::now() + Duration::from_secs(61));

    assert!(toasts.is_alive(id));
}

#[test]
fn colored_countdown_uses_timed_lifetime_not_task_linger() {
    let mut toasts = toasts();
    let id = toasts.push_colored_persistent(
        "Startup",
        vec!["Disk usage 100%".to_string()],
        vec![Color::Green],
    );

    assert!(toasts.update_colored(id, vec!["Disk usage 100%".to_string()], vec![Color::Green],));
    assert!(toasts.start_colored_countdown(id, Duration::from_secs(5)));

    let view = toasts
        .active_now()
        .into_iter()
        .find(|toast| toast.title() == "Startup")
        .unwrap_or_else(|| std::process::abort());
    assert_eq!(view.linger_progress(), None);
    assert!(view.remaining_secs().is_some());
    assert_eq!(view.body_line_colors(), Some(&[Color::Green][..]));
}

#[test]
fn dismiss_does_not_restart_an_already_exiting_animation() {
    let mut toasts = toasts();
    let task = toasts.start_task("scan", "running");
    let id = toasts
        .toast_for_task(task)
        .unwrap_or_else(|| std::process::abort())
        .id();

    // First dismiss: phase transitions to Exiting with the
    // initial started_at.
    assert!(toasts.dismiss(id));
    let first_started_at = match toasts
        .toast_for_task(task)
        .unwrap_or_else(|| std::process::abort())
        .phase
    {
        ToastPhase::Exiting { started_at } => started_at,
        ToastPhase::Visible => std::process::abort(),
    };

    // Spin a touch to make sure Instant::now() advances, then
    // dismiss again — the started_at must not reset.
    std::thread::sleep(Duration::from_millis(2));
    assert!(toasts.dismiss(id));
    let second_started_at = match toasts
        .toast_for_task(task)
        .unwrap_or_else(|| std::process::abort())
        .phase
    {
        ToastPhase::Exiting { started_at } => started_at,
        ToastPhase::Visible => std::process::abort(),
    };
    assert_eq!(first_started_at, second_started_at);
}

#[test]
fn user_dismissed_task_toast_is_not_revived_by_reactivate() {
    let mut toasts = toasts();
    let task = toasts.start_task("scan", "running");

    // User clicks [x].
    let toast_id = toasts
        .toast_for_task(task)
        .unwrap_or_else(|| std::process::abort())
        .id();
    assert!(toasts.dismiss(toast_id));

    // Tracker keeps reporting work; we ask reactivate_task to
    // re-show the toast. The user-dismissed flag suppresses
    // reactivation.
    assert_eq!(
        toasts.reactivate_task(task),
        super::ReactivateOutcome::DismissedByUser,
    );
    let toast = toasts
        .toast_for_task(task)
        .unwrap_or_else(|| std::process::abort());
    assert!(matches!(toast.phase, ToastPhase::Exiting { .. }));
    assert_eq!(toast.dismissal, ToastDismissal::ClosedByUser);
}

fn toasts_with_linger(linger_secs: f64) -> Toasts<TestApp> {
    let mut t = Toasts::<TestApp>::new();
    t.settings_mut().finished_task_visible =
        crate::ToastDuration::try_from_secs("test", linger_secs)
            .unwrap_or_else(|_| std::process::abort());
    t
}

#[test]
fn reactivate_task_revives_non_dismissed_finished_toast() {
    // Linger covers an item so finish_task records the toast
    // as still-finished rather than instantly-zero, which is
    // what `reactivate_task` is meant to recover from.
    let mut toasts = toasts_with_linger(30.0);
    let task = toasts.start_task("scan", "running");
    assert!(toasts.set_tracked_items(task, &[TrackedItem::new("a", "a")]));
    assert!(toasts.finish_task(task));

    assert_eq!(
        toasts.reactivate_task(task),
        super::ReactivateOutcome::Revived,
    );
    let toast = toasts
        .toast_for_task(task)
        .unwrap_or_else(|| std::process::abort());
    assert!(matches!(toast.phase, ToastPhase::Visible));
}

#[test]
fn reactivate_task_returns_not_found_for_unknown_task() {
    let mut toasts = toasts();
    let task = toasts.start_task("scan", "running");
    // No tracked items → finish_task uses Duration::ZERO.
    assert!(toasts.finish_task(task));
    let after_linger = Instant::now() + Duration::from_secs(2);
    toasts.prune(after_linger);

    let stale_task = ToastTaskId(99);
    assert_eq!(
        toasts.reactivate_task(stale_task),
        super::ReactivateOutcome::NotFound,
    );
}

#[test]
fn task_toast_lingers_after_finish_then_prunes() {
    let mut toasts = toasts_with_linger(1.0);
    let task = toasts.start_task("scan", "running");
    // Tracked item is what makes `finish_task` honor the
    // settings-driven linger.
    assert!(toasts.set_tracked_items(task, &[TrackedItem::new("a", "a")]));

    assert!(toasts.finish_task(task));
    toasts.prune(Instant::now());
    assert!(toasts.is_task_finished(task));

    let after_linger = Instant::now() + Duration::from_secs(2);
    toasts.prune(after_linger);
    toasts.prune(after_linger + Duration::from_secs(1));

    assert!(!toasts.is_task_finished(task));
}

#[test]
fn tracked_items_prune_after_linger() {
    let mut toasts = toasts_with_linger(0.0);
    let task = toasts.start_task("scan", "running");
    let item = TrackedItem::new("repo", "repo");
    assert!(toasts.set_tracked_items(task, &[item]));
    assert_eq!(toasts.tracked_item_count(task), 1);

    assert!(toasts.mark_tracked_item_completed(task, "repo"));
    toasts.prune_tracked_items(Instant::now() + Duration::from_secs(1));

    assert_eq!(toasts.tracked_item_count(task), 0);
}

#[test]
fn focused_toast_command_returns_action_payload() {
    #[derive(Clone, Debug, Eq, PartialEq)]
    enum ToastAction {
        Open,
    }

    struct ActionApp {
        framework: Framework<Self>,
    }

    impl AppContext for ActionApp {
        type AppPaneId = TestPaneId;
        type ToastAction = ToastAction;

        fn framework(&self) -> &Framework<Self> { &self.framework }
        fn framework_mut(&mut self) -> &mut Framework<Self> { &mut self.framework }
    }

    let mut toasts = Toasts::<ActionApp>::new();
    let _ = toasts.push_with_action("open", "path", ToastAction::Open);

    let command = toasts.handle_key_command(&KeyBind::from(KeyCode::Enter));

    assert_eq!(
        command,
        (
            KeyOutcome::Consumed,
            ToastCommand::Activate(ToastAction::Open)
        )
    );
}

#[test]
fn new_toasts_do_not_move_existing_focus() {
    let mut toasts = toasts();
    let first = toasts.push("first", "body");
    let _second = toasts.push("second", "body");

    assert_eq!(toasts.focused_id(), Some(first));
}

#[test]
fn toasts_can_live_on_framework() {
    let mut app = TestApp {
        framework: Framework::new(FocusedPane::App(TestPaneId::Main)),
    };
    let _ = app.framework.toasts.push("hello", "body");

    assert!(app.framework.toasts.has_active());
}

#[test]
fn marking_last_item_completed_auto_finishes_task_toast() {
    let mut toasts = toasts_with_linger(5.0);
    let task = toasts.start_task("phase", "body");
    assert!(toasts.set_tracked_items(
        task,
        &[TrackedItem::new("a", "a"), TrackedItem::new("b", "b")],
    ));

    assert!(toasts.mark_tracked_item_completed(task, "a"));
    assert!(
        !toasts.is_task_finished(task),
        "auto-finish must not fire while any item is incomplete",
    );

    assert!(toasts.mark_tracked_item_completed(task, "b"));
    assert!(
        toasts.is_task_finished(task),
        "marking the final item completed must transition the toast to Finished",
    );
}

#[test]
fn auto_finish_does_not_fire_with_pending_items() {
    let mut toasts = toasts_with_linger(5.0);
    let task = toasts.start_task("phase", "body");
    assert!(toasts.set_tracked_items(
        task,
        &[
            TrackedItem::new("a", "a"),
            TrackedItem::new("b", "b"),
            TrackedItem::new("c", "c"),
        ],
    ));

    assert!(toasts.mark_tracked_item_completed(task, "a"));
    assert!(toasts.mark_tracked_item_completed(task, "b"));
    assert!(!toasts.is_task_finished(task));
}

#[test]
fn auto_finish_does_not_fire_on_zero_item_task_toast() {
    let mut toasts = toasts_with_linger(5.0);
    let task = toasts.start_task("empty", "body");

    // No items were ever added. Auto-finish must not fire on its
    // own — an empty task toast stays Running until the embedding
    // calls `finish_task` explicitly.
    assert!(!toasts.is_task_finished(task));

    assert!(toasts.finish_task(task));
    assert!(toasts.is_task_finished(task));
}

#[test]
fn finish_task_anchors_finished_at_to_last_item_completion() {
    let mut toasts = toasts_with_linger(5.0);
    let task = toasts.start_task("phase", "body");
    assert!(toasts.set_tracked_items(task, &[TrackedItem::new("only", "only")]));

    assert!(toasts.mark_tracked_item_completed(task, "only"));
    let toast = toasts
        .toast_for_task(task)
        .unwrap_or_else(|| std::process::abort());
    let ToastLifetime::Task {
        status:
            ToastTaskStatus::Finished {
                finished_at: original_finished_at,
                ..
            },
        ..
    } = toast.lifetime
    else {
        std::process::abort();
    };

    std::thread::sleep(Duration::from_millis(5));
    // Calling finish_task again must not bump `finished_at` forward —
    // the anchor is `max(item.completed_at)`, which hasn't moved
    // because no item was re-marked. The countdown stays stable.
    assert!(toasts.finish_task(task));

    let toast = toasts
        .toast_for_task(task)
        .unwrap_or_else(|| std::process::abort());
    let ToastLifetime::Task {
        status:
            ToastTaskStatus::Finished {
                finished_at: later_finished_at,
                ..
            },
        ..
    } = toast.lifetime
    else {
        std::process::abort();
    };
    assert_eq!(original_finished_at, later_finished_at);
}

#[test]
fn adding_incomplete_item_reverts_finished_toast_to_running() {
    let mut toasts = toasts_with_linger(5.0);
    let task = toasts.start_task("phase", "body");
    assert!(toasts.set_tracked_items(task, &[TrackedItem::new("first", "first")]));

    // Mark the only item completed → toast auto-finishes.
    assert!(toasts.mark_tracked_item_completed(task, "first"));
    assert!(toasts.is_task_finished(task));

    // A new incomplete item arrives after auto-finish (e.g. a
    // tracker queues a late phase). The toast must revert to
    // Running so the countdown re-anchors when the new item
    // eventually completes.
    assert!(toasts.add_new_tracked_items(task, &[TrackedItem::new("late", "late")]));
    assert!(!toasts.is_task_finished(task));

    // When the late item finally completes, auto-finish re-fires.
    assert!(toasts.mark_tracked_item_completed(task, "late"));
    assert!(toasts.is_task_finished(task));
}

#[test]
fn late_completion_extends_finished_at_past_original_anchor() {
    let mut toasts = toasts_with_linger(5.0);
    let task = toasts.start_task("phase", "body");
    assert!(toasts.set_tracked_items(task, &[TrackedItem::new("first", "first")]));

    assert!(toasts.mark_tracked_item_completed(task, "first"));
    let toast = toasts
        .toast_for_task(task)
        .unwrap_or_else(|| std::process::abort());
    let ToastLifetime::Task {
        status:
            ToastTaskStatus::Finished {
                finished_at: anchor_after_first,
                ..
            },
        ..
    } = toast.lifetime
    else {
        std::process::abort();
    };

    // Add a second incomplete item, sleep, mark it completed.
    assert!(toasts.add_new_tracked_items(task, &[TrackedItem::new("late", "late")]));
    std::thread::sleep(Duration::from_millis(5));
    assert!(toasts.mark_tracked_item_completed(task, "late"));

    let toast = toasts
        .toast_for_task(task)
        .unwrap_or_else(|| std::process::abort());
    let ToastLifetime::Task {
        status:
            ToastTaskStatus::Finished {
                finished_at: anchor_after_late,
                ..
            },
        ..
    } = toast.lifetime
    else {
        std::process::abort();
    };
    assert!(
        anchor_after_late > anchor_after_first,
        "finished_at must move forward when a later item completes",
    );
}

#[test]
fn task_toast_skips_exit_animation_after_countdown() {
    let mut toasts = toasts_with_linger(0.0);
    let task = toasts.start_task("phase", "body");
    assert!(toasts.set_tracked_items(task, &[TrackedItem::new("only", "only")]));
    assert!(toasts.mark_tracked_item_completed(task, "only"));
    // finished_at + linger(=0) <= now, so should_exit is true on
    // the next prune. Task toasts skip the Exiting render phase
    // entirely: the toast is removed in the same prune pass.
    toasts.prune(Instant::now());
    assert!(!toasts.is_task_finished(task));
    assert!(toasts.toast_for_task(task).is_none());
}

#[test]
fn restarting_a_completed_item_reverts_finished_toast_to_running() {
    let mut toasts = toasts_with_linger(5.0);
    let task = toasts.start_task("phase", "body");
    let key = TrackedItemKey::new("a");
    assert!(toasts.set_tracked_items(task, &[TrackedItem::new("a", key.clone())]));
    assert!(toasts.mark_tracked_item_completed(task, "a"));
    assert!(toasts.is_task_finished(task));

    // Restarting clears the item's `completed_at` — the toast
    // reverts to Running until the item completes again.
    assert!(toasts.restart_tracked_item(task, &key, Instant::now()));
    assert!(!toasts.is_task_finished(task));
}
