use super::{
    archive::NextRuntimeArchiveId,
    archive::next_runtime_archive_id_request,
    archive::workspace_archive_label,
    build_partial_timeout_messages, effective_max_tool_calls_for_turn,
    resolve_effective_turn_timeout_secs, should_attempt_requesting_timeout_recovery,
    support::{
        TurnHistoryCheckpoint, build_tracked_file_freshness_note,
        build_unrelated_dirty_worktree_note, checkpoint_session_archive_start,
        latest_assistant_result_text, prepare_resume_bootstrap_without_archive,
        remove_transient_system_notes, take_pending_resumed_user_prompt,
    },
};
use crate::agent::agents::ResumeSession;
use crate::agent::runloop::git::normalize_workspace_path;
use crate::agent::runloop::unified::run_loop_context::TurnPhase;
use chrono::Utc;
use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
use vtcode_core::core::threads::ArchivedSessionIntent;
use vtcode_core::exec::events::ThreadCompletionSubtype;
use vtcode_core::hooks::SessionEndReason;
use vtcode_core::llm::provider::MessageRole;
use vtcode_core::utils::session_archive::{
    SessionArchive, SessionArchiveMetadata, SessionListing, SessionMessage, SessionSnapshot,
};

fn resume_session(intent: ArchivedSessionIntent) -> ResumeSession {
    let listing = SessionListing {
        path: PathBuf::from("/tmp/session-source.json"),
        snapshot: SessionSnapshot {
            metadata: SessionArchiveMetadata::new(
                "workspace",
                "/tmp/workspace",
                "model",
                "provider",
                "theme",
                "medium",
            ),
            started_at: Utc::now(),
            ended_at: Utc::now(),
            total_messages: 1,
            distinct_tools: Vec::new(),
            transcript: Vec::new(),
            messages: vec![SessionMessage::new(MessageRole::User, "hello")],
            progress: None,
            error_logs: Vec::new(),
        },
    };

    ResumeSession::from_listing(&listing, intent)
}

#[test]
fn turn_timeout_respects_tool_wall_clock_budget() {
    assert_eq!(resolve_effective_turn_timeout_secs(300, 600), 660);
}

#[test]
fn turn_timeout_keeps_higher_configured_value() {
    assert_eq!(resolve_effective_turn_timeout_secs(900, 600), 900);
}

#[test]
fn turn_timeout_includes_full_llm_attempt_grace() {
    assert_eq!(resolve_effective_turn_timeout_secs(360, 360), 432);
}

#[test]
fn turn_timeout_expands_buffer_for_large_configs() {
    assert_eq!(resolve_effective_turn_timeout_secs(600, 600), 720);
}

#[test]
fn plan_mode_applies_tool_call_floor() {
    assert_eq!(effective_max_tool_calls_for_turn(32, true), 48);
    assert_eq!(effective_max_tool_calls_for_turn(64, true), 64);
}

#[test]
fn zero_tool_call_limit_stays_unlimited_in_all_modes() {
    assert_eq!(effective_max_tool_calls_for_turn(0, true), 0);
    assert_eq!(effective_max_tool_calls_for_turn(0, false), 0);
}

#[test]
fn edit_mode_keeps_configured_tool_call_limit() {
    assert_eq!(effective_max_tool_calls_for_turn(32, false), 32);
}

#[test]
fn requesting_partial_timeout_recovery_message_mentions_continuation() {
    let (timeout_message, timeout_error_message) =
        build_partial_timeout_messages(660, TurnPhase::Requesting, 25, 0, true);
    assert!(timeout_message.contains("continuing with a compacted tool-free recovery pass"));
    assert!(timeout_error_message.contains("Continuing with a compacted tool-free recovery pass"));
}

#[test]
fn requesting_partial_timeout_without_recovery_mentions_retry_skip() {
    let (timeout_message, timeout_error_message) =
        build_partial_timeout_messages(660, TurnPhase::Requesting, 25, 0, false);
    assert!(timeout_message.contains("retry is skipped"));
    assert!(!timeout_message.contains("continuing with a compacted tool-free recovery pass"));
    assert!(!timeout_error_message.contains("Continuing with a compacted tool-free recovery pass"));
}

#[test]
fn take_pending_resumed_user_prompt_removes_trailing_user_message() {
    let mut history = vec![
        vtcode_core::llm::provider::Message::system("[Session Memory Envelope]".to_string()),
        vtcode_core::llm::provider::Message::user("what is this project".to_string()),
    ];

    let pending = take_pending_resumed_user_prompt(&mut history);

    assert_eq!(pending.as_deref(), Some("what is this project"));
    assert_eq!(history.len(), 1);
    assert_eq!(history[0].role, MessageRole::System);
}

#[test]
fn take_pending_resumed_user_prompt_handles_trailing_system_notes() {
    let mut history = vec![
        vtcode_core::llm::provider::Message::system("[Session Memory Envelope]".to_string()),
        vtcode_core::llm::provider::Message::user("what is this project".to_string()),
        vtcode_core::llm::provider::Message::system(
            "Recovered from interrupted session".to_string(),
        ),
    ];

    let pending = take_pending_resumed_user_prompt(&mut history);

    assert_eq!(pending.as_deref(), Some("what is this project"));
    assert_eq!(history.len(), 2);
    assert_eq!(history[0].role, MessageRole::System);
    assert_eq!(history[1].role, MessageRole::System);
}

#[test]
fn take_pending_resumed_user_prompt_ignores_completed_turns() {
    let mut history = vec![
        vtcode_core::llm::provider::Message::user("what is this project".to_string()),
        vtcode_core::llm::provider::Message::assistant(
            "VT Code is a Rust coding agent".to_string(),
        ),
        vtcode_core::llm::provider::Message::system("[Session Memory Envelope]".to_string()),
    ];

    let pending = take_pending_resumed_user_prompt(&mut history);

    assert!(pending.is_none());
    assert_eq!(history.len(), 3);
    assert_eq!(history[0].role, MessageRole::User);
    assert_eq!(history[1].role, MessageRole::Assistant);
}

#[test]
fn turn_history_checkpoint_truncates_appended_messages() {
    let mut history = vec![
        vtcode_core::llm::provider::Message::user("before".to_string()),
        vtcode_core::llm::provider::Message::assistant("baseline".to_string()),
    ];
    let checkpoint = TurnHistoryCheckpoint::capture(&history);

    history.push(vtcode_core::llm::provider::Message::assistant(
        "during turn".to_string(),
    ));
    history.push(vtcode_core::llm::provider::Message::tool_response(
        "call-1".to_string(),
        "{\"ok\":true}".to_string(),
    ));

    checkpoint.rollback(&mut history);

    assert_eq!(history.len(), 2);
    assert_eq!(history[0].content.as_text(), "before");
    assert_eq!(history[1].content.as_text(), "baseline");
}

#[test]
fn turn_history_checkpoint_preserves_preexisting_history_prefix() {
    let mut history = vec![
        vtcode_core::llm::provider::Message::system("system".to_string()),
        vtcode_core::llm::provider::Message::user("request".to_string()),
        vtcode_core::llm::provider::Message::assistant("response".to_string()),
    ];
    let expected_prefix = history.clone();
    let checkpoint = TurnHistoryCheckpoint::capture(&history);

    history.push(vtcode_core::llm::provider::Message::assistant(
        "retryable append".to_string(),
    ));

    checkpoint.rollback(&mut history);

    assert_eq!(history, expected_prefix);
}

#[test]
fn requesting_timeout_without_tool_activity_omits_autonomous_recovery_note() {
    let (timeout_message, timeout_error_message) =
        build_partial_timeout_messages(660, TurnPhase::Requesting, 0, 0, false);
    assert!(!timeout_message.contains("continuing with a compacted tool-free recovery pass"));
    assert!(!timeout_error_message.contains("Continuing with a compacted tool-free recovery pass"));
}

#[test]
fn requesting_timeout_recovery_only_runs_once() {
    assert!(should_attempt_requesting_timeout_recovery(
        TurnPhase::Requesting,
        true,
        false,
    ));
    assert!(!should_attempt_requesting_timeout_recovery(
        TurnPhase::Requesting,
        true,
        true,
    ));
    assert!(!should_attempt_requesting_timeout_recovery(
        TurnPhase::ExecutingTools,
        true,
        false,
    ));
}

#[test]
fn transient_system_note_cleanup_removes_by_content_from_latest_match() {
    let note = "Freshness note: file changed".to_string();
    let older = vtcode_core::llm::provider::Message::system(note.clone());
    let transient = vtcode_core::llm::provider::Message::system(note.clone());
    let mut history = vec![
        older,
        vtcode_core::llm::provider::Message::assistant("summary".to_string()),
        transient,
        vtcode_core::llm::provider::Message::user("preserved".to_string()),
    ];

    remove_transient_system_notes(&mut history, std::slice::from_ref(&note));

    assert_eq!(history.len(), 3);
    assert_eq!(history[0].content.as_text(), note);
    assert_eq!(history[1].content.as_text(), "summary");
    assert_eq!(history[2].content.as_text(), "preserved");
}

#[test]
fn workspace_archive_label_uses_directory_name() {
    assert_eq!(workspace_archive_label(Path::new("/tmp/demo")), "demo");
}

#[test]
fn tracked_file_freshness_note_uses_relative_paths_and_reread_guidance() {
    let note = build_tracked_file_freshness_note(
        Path::new("/tmp/workspace"),
        &[
            PathBuf::from("/tmp/workspace/src/main.rs"),
            PathBuf::from("/tmp/workspace/docs/project/TODO.md"),
        ],
    )
    .expect("freshness note");

    assert!(note.contains("Freshness note"));
    assert!(note.contains("- src/main.rs"));
    assert!(note.contains("- docs/project/TODO.md"));
    assert!(note.contains("Re-read these files before relying on earlier content"));
}

fn init_git_repo() -> TempDir {
    let temp = TempDir::new().expect("temp dir");
    let run = |args: &[&str]| {
        let status = Command::new("git")
            .args(args)
            .current_dir(temp.path())
            .status()
            .expect("git command");
        assert!(status.success(), "git command failed: {args:?}");
    };

    run(&["init"]);
    run(&["config", "user.name", "VT Code"]);
    run(&["config", "user.email", "vtcode@example.com"]);
    temp
}

fn seed_dirty_repo() -> (TempDir, PathBuf) {
    let repo = init_git_repo();
    let path = repo.path().join("docs/project/TODO.md");
    fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
    fs::write(&path, "before\n").expect("write");

    let run = |args: &[&str]| {
        let status = Command::new("git")
            .args(args)
            .current_dir(repo.path())
            .status()
            .expect("git command");
        assert!(status.success(), "git command failed: {args:?}");
    };

    run(&["add", "."]);
    run(&["commit", "-m", "test: seed repo"]);
    fs::write(&path, "after\n").expect("write");
    (repo, path)
}

#[test]
fn unrelated_dirty_worktree_note_uses_relative_paths_and_user_owned_guidance() {
    let (repo, path) = seed_dirty_repo();

    let note = build_unrelated_dirty_worktree_note(repo.path(), &BTreeSet::new())
        .expect("note build")
        .expect("note");

    assert!(note.contains("Workspace note"));
    assert!(note.contains("docs/project/TODO.md"));
    assert!(note.contains("user-owned changes"));
    assert!(!note.contains(&path.display().to_string()));
}

#[test]
fn unrelated_dirty_worktree_note_skips_agent_touched_files() {
    let (repo, path) = seed_dirty_repo();
    let mut touched_paths = BTreeSet::new();
    touched_paths.insert(normalize_workspace_path(repo.path(), &path));

    let note =
        build_unrelated_dirty_worktree_note(repo.path(), &touched_paths).expect("note build");

    assert!(note.is_none());
}

#[test]
fn next_runtime_archive_id_request_reuses_existing_id_for_resume() {
    let resume = resume_session(ArchivedSessionIntent::ResumeInPlace);

    assert_eq!(
        next_runtime_archive_id_request(Path::new("/tmp/workspace"), Some(&resume)),
        NextRuntimeArchiveId::Existing("session-source".to_string())
    );
}

#[test]
fn next_runtime_archive_id_request_reserves_for_fork_and_new_session() {
    let resume = resume_session(ArchivedSessionIntent::ForkNewArchive {
        custom_suffix: Some("branch".to_string()),
        summarize: false,
    });

    assert_eq!(
        next_runtime_archive_id_request(Path::new("/tmp/workspace"), Some(&resume)),
        NextRuntimeArchiveId::Reserve {
            workspace_label: "workspace".to_string(),
            custom_suffix: Some("branch".to_string()),
        }
    );
    assert_eq!(
        next_runtime_archive_id_request(Path::new("/tmp/workspace"), None),
        NextRuntimeArchiveId::Reserve {
            workspace_label: "workspace".to_string(),
            custom_suffix: None,
        }
    );
}

#[test]
fn resume_bootstrap_without_archive_reuses_identifier_for_in_place_resume() {
    let resume = resume_session(ArchivedSessionIntent::ResumeInPlace);
    let (bootstrap, thread_id) = prepare_resume_bootstrap_without_archive(
        &resume,
        SessionArchiveMetadata::new(
            "workspace",
            "/tmp/workspace",
            "model",
            "provider",
            "theme",
            "medium",
        ),
        None,
    );

    assert_eq!(thread_id, "session-source");
    assert_eq!(
        bootstrap
            .metadata
            .as_ref()
            .map(|meta| meta.workspace_label.as_str()),
        Some("workspace")
    );
}

#[test]
fn resume_bootstrap_without_archive_prefers_reserved_identifier_for_forks() {
    let resume = resume_session(ArchivedSessionIntent::ForkNewArchive {
        custom_suffix: Some("branch".to_string()),
        summarize: false,
    });
    let (_, thread_id) = prepare_resume_bootstrap_without_archive(
        &resume,
        SessionArchiveMetadata::new(
            "workspace",
            "/tmp/workspace",
            "model",
            "provider",
            "theme",
            "medium",
        ),
        Some("reserved-session-id".to_string()),
    );

    assert_eq!(thread_id, "reserved-session-id");
}

#[test]
fn resume_bootstrap_without_archive_preserves_compatible_prompt_cache_lineage() {
    let listing = SessionListing {
        path: PathBuf::from("/tmp/session-source.json"),
        snapshot: SessionSnapshot {
            metadata: SessionArchiveMetadata::new(
                "workspace",
                "/tmp/workspace",
                "model",
                "provider",
                "theme",
                "medium",
            )
            .with_prompt_cache_lineage_id("lineage-123"),
            started_at: Utc::now(),
            ended_at: Utc::now(),
            total_messages: 1,
            distinct_tools: Vec::new(),
            transcript: Vec::new(),
            messages: vec![SessionMessage::new(MessageRole::User, "hello")],
            progress: None,
            error_logs: Vec::new(),
        },
    };
    let resume = ResumeSession::from_listing(&listing, ArchivedSessionIntent::ResumeInPlace);
    let (bootstrap, _) = prepare_resume_bootstrap_without_archive(
        &resume,
        SessionArchiveMetadata::new(
            "workspace",
            "/tmp/workspace",
            "model",
            "provider",
            "other-theme",
            "high",
        ),
        None,
    );

    assert_eq!(
        bootstrap
            .metadata
            .as_ref()
            .and_then(|meta| meta.prompt_cache_lineage_id.as_deref()),
        Some("lineage-123")
    );
}

#[test]
fn thread_completion_status_matches_public_contract() {
    assert_eq!(
        SessionEndReason::Completed.thread_completion_status(false),
        ("completed", ThreadCompletionSubtype::Success)
    );
    assert_eq!(
        SessionEndReason::NewSession.thread_completion_status(false),
        ("new_session", ThreadCompletionSubtype::Success)
    );
    assert_eq!(
        SessionEndReason::Exit.thread_completion_status(false),
        ("exit", ThreadCompletionSubtype::Cancelled)
    );
    assert_eq!(
        SessionEndReason::Cancelled.thread_completion_status(false),
        ("cancelled", ThreadCompletionSubtype::Cancelled)
    );
    assert_eq!(
        SessionEndReason::Error.thread_completion_status(false),
        ("error", ThreadCompletionSubtype::ErrorDuringExecution)
    );
    assert_eq!(
        SessionEndReason::Completed.thread_completion_status(true),
        (
            "budget_limit_reached",
            ThreadCompletionSubtype::ErrorMaxBudgetUsd,
        )
    );
}

#[test]
fn latest_assistant_result_text_uses_latest_nonempty_assistant_message() {
    let messages = vec![
        vtcode_core::llm::provider::Message::user("hello".to_string()),
        vtcode_core::llm::provider::Message::assistant(" first ".to_string()),
        vtcode_core::llm::provider::Message::tool_response(
            "call-1".to_string(),
            "{\"ok\":true}".to_string(),
        ),
        vtcode_core::llm::provider::Message::assistant(" final answer ".to_string()),
    ];

    assert_eq!(
        latest_assistant_result_text(&messages),
        Some("final answer".to_string())
    );
}

#[tokio::test]
async fn checkpoint_session_archive_start_writes_initial_snapshot() {
    let temp_dir = tempfile::tempdir().expect("tempdir");
    let archive_path = temp_dir.path().join("session-vtcode-test-start.json");
    let metadata = SessionArchiveMetadata::new(
        "workspace",
        "/tmp/workspace",
        "model",
        "provider",
        "theme",
        "medium",
    );
    let archive = SessionArchive::resume_from_listing(
        &SessionListing {
            path: archive_path.clone(),
            snapshot: SessionSnapshot {
                metadata: metadata.clone(),
                started_at: Utc::now(),
                ended_at: Utc::now(),
                total_messages: 0,
                distinct_tools: Vec::new(),
                transcript: Vec::new(),
                messages: Vec::new(),
                progress: None,
                error_logs: Vec::new(),
            },
        },
        metadata.clone(),
    );
    let thread_manager = vtcode_core::core::threads::ThreadManager::new();
    let thread_handle = thread_manager.start_thread_with_identifier(
        "session-vtcode-test-start",
        vtcode_core::core::threads::ThreadBootstrap::new(Some(metadata)).with_messages(vec![
            vtcode_core::llm::provider::Message::user("hello".to_string()),
        ]),
    );

    checkpoint_session_archive_start(&archive, &thread_handle)
        .await
        .expect("startup checkpoint");

    let snapshot: SessionSnapshot =
        serde_json::from_str(&fs::read_to_string(archive_path).expect("read archive"))
            .expect("parse archive");
    assert_eq!(snapshot.total_messages, 1);
    assert_eq!(snapshot.messages.len(), 1);
    assert!(snapshot.progress.is_some());
}
