//! End-to-end exercise of the Plan-4 write pipeline. Two flows:
//!
//! 1. Dry-run mode: test-DB with `allow_writes_on_test_db=true`. Asserts the
//!    executor was NOT called and `WriteOutcome { dry_run: true }`.
//! 2. Recording-executor live mode: a `RecordingExecutor` is injected via
//!    `AppStateOptions.executor_override`. Asserts the executor recorded
//!    exactly one URL that parses as a valid `things:///json` URL containing
//!    the title; verify times out (no real Things app), yielding
//!    `WriteOutcome { dry_run: false, verified: false }`.

use std::sync::Arc;

use things_mcp::core::reader::fixture::build_fixture;
use things_mcp::core::writer::executor::{Executor, RecordingExecutor};
use things_mcp::state::{AppState, AppStateOptions};
use things_mcp::tools::todos::{things_add_todo, AddTodoArgs};

async fn build_state(
    allow_writes_on_test_db: bool,
    executor_override: Option<Arc<dyn Executor>>,
) -> AppState {
    let tmp = tempfile::tempdir().unwrap();
    let db = tmp.path().join("things.sqlite");
    build_fixture(&db).unwrap();
    let state = AppState::build(AppStateOptions {
        env_db_path: Some(db.clone()),
        home_dir: tmp.path().to_path_buf(),
        config_path: tmp.path().join("config.toml"),
        allow_writes_on_test_db,
        executor_override,
        applescript_override: None,
    })
    .await
    .unwrap();
    std::mem::forget(tmp);
    state
}

#[tokio::test]
async fn add_todo_dry_run_does_not_call_executor() {
    // No executor override — the test-DB safety gate short-circuits before
    // the executor would be called anyway. Tighter coverage: also pass a
    // recording executor and assert it stays empty.
    let recorder = Arc::new(RecordingExecutor::new());
    let state = build_state(true, Some(recorder.clone() as Arc<dyn Executor>)).await;
    let out = things_add_todo(
        state,
        AddTodoArgs {
            title: "Pretend buy bread".into(),
            ..Default::default()
        },
    )
    .await
    .unwrap();
    assert!(out.dry_run, "expected dry_run=true in test-DB mode");
    assert!(!out.verified);
    assert_eq!(out.action, "add_todo");
    assert!(out.id.is_none());
    assert_eq!(recorder.urls().len(), 0, "executor must not be called in dry-run");
}

#[tokio::test]
async fn add_todo_plumbs_optional_fields_through_dry_run() {
    // Asserts that args with tags/notes/etc. round-trip through the tool
    // layer cleanly. The dry-run path still constructs the Operation but
    // short-circuits before the executor — so the recording executor stays
    // empty. End-to-end coverage of the Live executor call lives in the
    // Writer unit test (Task 6); the integration boundary covers wiring.
    let recorder = Arc::new(RecordingExecutor::new());
    let state = build_state(true, Some(recorder.clone() as Arc<dyn Executor>)).await;
    let out = things_add_todo(
        state,
        AddTodoArgs {
            title: "Through the chassis".into(),
            tags: vec!["E2E".into()],
            notes: Some("via integration test".into()),
            ..Default::default()
        },
    )
    .await
    .unwrap();
    assert!(out.dry_run);
    assert!(!out.verified);
    assert_eq!(out.action, "add_todo");
    assert_eq!(recorder.urls().len(), 0);
}
