#![allow(clippy::unwrap_used, clippy::expect_used)]
//! E2E: write a file in turn 1 (session A), resume into a fresh App (session A),
//! and edit the same file without an explicit re-read — the hydration path
//! restored `read_files` from history.

use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

use async_trait::async_trait;
use capo_agent::{AppBuilder, Auth, SessionId, SessionLookup, SessionPaths, Settings, UserMessage};
use futures::StreamExt;
use motosan_agent_loop::{
    ChatOutput, FileSessionStore, LlmClient, LlmResponse, Message, SessionEntry, SessionStore,
    ToolCallItem, ToolCallRef,
};
use motosan_agent_tool::ToolDef;
use tempfile::tempdir;

/// Pre-scripted LLM: first call emits an `edit` ToolCall, subsequent calls
/// (after the tool result feeds back) just answer. Without replay hydration,
/// the EditTool would reject the call because `greet.txt` was never read in
/// this run.
struct WriteThenEditLlm {
    turn: AtomicUsize,
    target_path: String,
}

#[async_trait]
impl LlmClient for WriteThenEditLlm {
    async fn chat(
        &self,
        _messages: &[Message],
        _tools: &[ToolDef],
    ) -> motosan_agent_loop::Result<ChatOutput> {
        let turn = self.turn.fetch_add(1, Ordering::SeqCst);
        let response = match turn {
            0 => LlmResponse::ToolCalls(vec![ToolCallItem {
                id: "edit-1".into(),
                name: "edit".into(),
                args: serde_json::json!({
                    "path": self.target_path,
                    "old_string": "hello",
                    "new_string": "goodbye",
                }),
            }]),
            _ => LlmResponse::Message("done".into()),
        };
        Ok(ChatOutput::new(response))
    }
}

#[tokio::test]
async fn resume_then_edit_succeeds_without_re_read() {
    let dir = tempdir().unwrap();
    let cwd = dir.path().join("project");
    std::fs::create_dir_all(&cwd).unwrap();

    let target = cwd.join("greet.txt");
    std::fs::write(&target, "hello").unwrap();

    // Set up session storage manually (we don't use the binary here).
    let agent_dir = dir.path().join("agent");
    let session_paths = SessionPaths::for_cwd(agent_dir.clone(), &cwd);
    session_paths.ensure_bucket().unwrap();

    let store: Arc<dyn SessionStore> =
        Arc::new(FileSessionStore::new(session_paths.bucket_dir.clone()));

    // ── PHASE 1: pre-seed a session that contains a successful WRITE of `greet.txt`.
    // We bypass the full agent path here and write a synthetic SessionEntry sequence:
    // an Assistant ToolCall(write) followed by the matching Tool result. The replay
    // hydration in build_with_session only inspects Assistant ToolCalls; the
    // Tool result is just for shape completeness.
    let session_id = SessionId::new();
    store
        .append_entry(
            session_id.as_str(),
            &SessionEntry::message(Message::assistant_with_tool_call(
                "",
                ToolCallRef {
                    id: "write-1".into(),
                    name: "write".into(),
                    args: serde_json::json!({ "path": "greet.txt", "content": "hello" }),
                },
            )),
        )
        .await
        .unwrap();
    store
        .append_entry(
            session_id.as_str(),
            &SessionEntry::message(Message::tool_result("write-1", "wrote 5 bytes")),
        )
        .await
        .unwrap();
    store.flush(session_id.as_str()).await.unwrap();

    // ── PHASE 2: resume + run a turn that edits the same path. Without hydration
    // the EditTool would reject ("refusing to edit X without reading it first").
    let llm = WriteThenEditLlm {
        turn: AtomicUsize::new(0),
        target_path: "greet.txt".to_string(),
    };

    let mut settings = Settings::default();
    // Sentinel: disable autocompact to keep the test deterministic.
    settings.session.compact_at_context_pct = 0.0;

    let app = AppBuilder::new()
        .with_settings(settings)
        .with_auth(Auth::default())
        .with_cwd(&cwd)
        .with_builtin_tools()
        .disable_context_discovery()
        .with_llm(Arc::new(llm))
        .with_session_store(Arc::clone(&store))
        .build_with_session(Some(session_id.clone()))
        .await
        .expect("resume build");

    let events: Vec<capo_agent::UiEvent> = app
        .send_user_message(UserMessage::text("rewrite please"))
        .collect()
        .await;

    // Expect at least one ToolCallCompleted with !is_error for the edit.
    let edited_ok = events.iter().any(|e| {
        matches!(
            e,
            capo_agent::UiEvent::ToolCallCompleted { result, .. }
                if !result.is_error && result.text.contains("edit:")
        )
    });
    assert!(
        edited_ok,
        "expected successful EditTool result; got events: {events:?}"
    );

    // File on disk reflects the edit.
    let content = std::fs::read_to_string(&target).unwrap();
    assert_eq!(content, "goodbye");
}

#[tokio::test]
async fn session_lookup_round_trip_continue_then_resolve() {
    let dir = tempdir().unwrap();
    let cwd = dir.path().join("p");
    std::fs::create_dir_all(&cwd).unwrap();
    let agent_dir = dir.path().join("agent");
    let session_paths = SessionPaths::for_cwd(agent_dir, &cwd);
    session_paths.ensure_bucket().unwrap();

    let store = FileSessionStore::new(session_paths.bucket_dir.clone());
    store
        .append_entry(
            "01TESTABC0000000000000000A",
            &SessionEntry::message(Message::user("hi")),
        )
        .await
        .unwrap();
    store.flush("01TESTABC0000000000000000A").await.unwrap();

    // --continue
    let id = SessionLookup::most_recent(&session_paths).await.unwrap();
    assert_eq!(id.as_str(), "01TESTABC0000000000000000A");

    // --session <prefix>
    let id = SessionLookup::resolve(&session_paths, "01TEST")
        .await
        .unwrap();
    assert_eq!(id.as_str(), "01TESTABC0000000000000000A");
}
