use super::*;
use tempfile::tempdir;

fn utc(y: i32, m: u32, d: u32, hh: u32, mm: u32, ss: u32) -> DateTime<Utc> {
    Utc.with_ymd_and_hms(y, m, d, hh, mm, ss)
        .single()
        .expect("valid timestamp")
}

#[test]
fn loop_defaults_to_ten_minutes() {
    let parsed = parse_loop_command("check the build").expect("loop");
    assert_eq!(parsed.prompt, "check the build");
    assert_eq!(parsed.interval.seconds, 600);
    assert!(parsed.normalization_note.is_none());
}

#[test]
fn loop_parses_leading_interval() {
    let parsed = parse_loop_command("30m check the build").expect("loop");
    assert_eq!(parsed.prompt, "check the build");
    assert_eq!(parsed.interval.seconds, 30 * 60);
}

#[test]
fn loop_parses_trailing_every_clause() {
    let parsed = parse_loop_command("check the build every 2 hours").expect("loop");
    assert_eq!(parsed.prompt, "check the build");
    assert_eq!(parsed.interval.seconds, 2 * 60 * 60);
}

#[test]
fn loop_rounds_seconds_up_to_minutes() {
    let parsed = parse_loop_command("45s check again").expect("loop");
    assert_eq!(parsed.interval.seconds, 60);
    assert!(parsed.normalization_note.is_some());
}

#[test]
fn loop_rounds_unclean_minutes() {
    let parsed = parse_loop_command("7m check again").expect("loop");
    assert_eq!(parsed.interval.seconds, 6 * 60);
    assert!(parsed.normalization_note.is_some());
}

#[test]
fn cron5_supports_vixie_or_semantics() {
    let cron = Cron5::parse("0 9 15 * 1").expect("cron");
    let monday = Local
        .with_ymd_and_hms(2026, 3, 30, 9, 0, 0)
        .single()
        .expect("monday");
    let dom = Local
        .with_ymd_and_hms(2026, 4, 15, 9, 0, 0)
        .single()
        .expect("dom");
    assert!(cron.parsed().expect("parsed").matches(monday));
    assert!(cron.parsed().expect("parsed").matches(dom));
}

#[test]
fn cron5_rejects_extended_syntax() {
    Cron5::parse("0 9 ? * *").unwrap_err();
    Cron5::parse("0 9 * JAN *").unwrap_err();
    Cron5::parse("0 9 * * MON").unwrap_err();
}

#[test]
fn cron5_finds_next_matching_minute() {
    let cron = Cron5::parse("*/15 * * * *").expect("cron");
    let start = Local
        .with_ymd_and_hms(2026, 3, 28, 10, 7, 13)
        .single()
        .expect("start");
    let next = cron.next_after(start).expect("next").expect("some");
    assert_eq!(next.minute(), 15);
}

#[test]
fn reminder_language_detects_at_time() {
    let now = Local
        .with_ymd_and_hms(2026, 3, 28, 13, 0, 0)
        .single()
        .expect("now");
    let command =
        parse_session_language_command("remind me at 3pm to push the release branch", now)
            .expect("command")
            .expect("parsed");
    match command {
        SessionLanguageCommand::CreateOneShotPrompt { prompt, .. } => {
            assert_eq!(prompt, "push the release branch");
        }
        other => panic!("unexpected command: {other:?}"),
    }
}

#[test]
fn reminder_language_detects_relative_time() {
    let now = Local
        .with_ymd_and_hms(2026, 3, 28, 13, 0, 0)
        .single()
        .expect("now");
    let command = parse_session_language_command(
        "in 45 minutes, check whether the integration tests passed",
        now,
    )
    .expect("command")
    .expect("parsed");
    match command {
        SessionLanguageCommand::CreateOneShotPrompt { prompt, run_at } => {
            assert_eq!(prompt, "check whether the integration tests passed");
            assert_eq!(
                run_at,
                (now + ChronoDuration::minutes(45)).with_timezone(&Utc)
            );
        }
        other => panic!("unexpected command: {other:?}"),
    }
}

#[test]
fn session_scheduler_expires_recurring_tasks_after_final_fire() {
    let created_at = utc(2026, 3, 28, 0, 0, 0);
    let mut scheduler = SessionScheduler::new();
    scheduler
        .create_prompt_task(
            Some("heartbeat".to_string()),
            "check".to_string(),
            ScheduleSpec::FixedInterval(FixedInterval { seconds: 60 * 60 }),
            created_at,
        )
        .expect("create");
    let record = scheduler.tasks.values_mut().next().expect("task");
    record.runtime.next_base_run_at = Some(created_at + ChronoDuration::hours(72));
    record.runtime.next_run_at = Some(created_at + ChronoDuration::hours(72));
    let due = scheduler
        .collect_due_prompts(created_at + ChronoDuration::hours(72))
        .expect("collect");
    assert_eq!(due.len(), 1);
    assert!(scheduler.is_empty());
}

#[test]
fn session_scheduler_jitter_is_stable_for_task_id() {
    let definition = ScheduledTaskDefinition {
        id: "abcd1234".to_string(),
        name: "test".to_string(),
        schedule: ScheduleSpec::FixedInterval(FixedInterval { seconds: 600 }),
        action: ScheduledTaskAction::Prompt {
            prompt: "check".to_string(),
        },
        workspace: None,
        created_at: utc(2026, 3, 28, 0, 0, 0),
        expires_at: None,
    };
    let base = utc(2026, 3, 28, 1, 0, 0);
    let first = definition
        .schedule
        .jittered_fire_at(&definition.id, base)
        .expect("jitter");
    let second = definition
        .schedule
        .jittered_fire_at(&definition.id, base)
        .expect("jitter");
    assert_eq!(first, second);
}

#[test]
fn disable_cron_env_overrides_enabled_config() {
    test_env_overrides::set(Some("1"));
    assert!(!scheduled_tasks_enabled(true));
    test_env_overrides::set(None);
}

#[test]
fn durable_store_creates_and_lists_tasks() {
    let temp = tempdir().expect("tempdir");
    let store = DurableTaskStore::with_paths(SchedulerPaths {
        config_root: temp.path().join("cfg"),
        data_root: temp.path().join("data"),
    });
    let definition = ScheduledTaskDefinition::new(
        Some("daily".to_string()),
        ScheduleSpec::OneShot(OneShot {
            at: utc(2026, 3, 29, 9, 0, 0),
        }),
        ScheduledTaskAction::Reminder {
            message: "check release".to_string(),
        },
        None,
        utc(2026, 3, 28, 0, 0, 0),
        None,
    )
    .expect("definition");
    store.create(definition).expect("create");
    let tasks = store.list().expect("list");
    assert_eq!(tasks.len(), 1);
    assert_eq!(tasks[0].name, "daily");
}

#[test]
fn scheduled_workspace_resolution_expands_home_and_normalizes() {
    let resolved = resolve_scheduled_workspace_path_with_home(
        Path::new("~/projects/demo/../vtcode"),
        Some(Path::new("/tmp/home")),
    )
    .expect("resolve");
    assert_eq!(resolved, PathBuf::from("/tmp/home/projects/vtcode"));
}

#[test]
fn schedule_create_definition_normalizes_prompt_workspace() {
    let temp = tempdir().expect("tempdir");
    let workspace = temp.path().join("workspace");
    fs::create_dir_all(&workspace).expect("workspace");
    let definition = ScheduleCreateInput {
        name: None,
        prompt: Some("check build".to_string()),
        reminder: None,
        every: Some("15m".to_string()),
        cron: None,
        at: None,
        workspace: Some(workspace.join(".").join("nested").join("..")),
    }
    .build_definition(Local::now(), None)
    .expect("definition");

    assert_eq!(definition.workspace.as_deref(), Some(workspace.as_path()));
}

#[test]
fn schedule_create_definition_rejects_missing_prompt_workspace() {
    let error = ScheduleCreateInput {
        name: None,
        prompt: Some("check build".to_string()),
        reminder: None,
        every: Some("15m".to_string()),
        cron: None,
        at: None,
        workspace: Some(PathBuf::from("/path/that/does/not/exist")),
    }
    .build_definition(Local::now(), None)
    .expect_err("missing workspace should fail");

    assert!(
        error
            .to_string()
            .contains("Prompt task workspace does not exist or is not a directory")
    );
}

#[cfg(unix)]
#[tokio::test]
async fn scheduler_daemon_executes_due_prompt_task() {
    let temp = tempdir().expect("tempdir");
    let workspace = temp.path().join("workspace");
    fs::create_dir_all(&workspace).expect("workspace");
    let store = DurableTaskStore::with_paths(SchedulerPaths {
        config_root: temp.path().join("cfg"),
        data_root: temp.path().join("data"),
    });
    let now = Utc::now();
    let definition = ScheduledTaskDefinition::new(
        Some("hello".to_string()),
        ScheduleSpec::OneShot(OneShot {
            at: now - ChronoDuration::minutes(1),
        }),
        ScheduledTaskAction::Prompt {
            prompt: "say hello".to_string(),
        },
        Some(workspace),
        now - ChronoDuration::minutes(2),
        None,
    )
    .expect("definition");
    let summary = store.create(definition).expect("create");
    let executable = ["/usr/bin/true", "/bin/true"]
        .into_iter()
        .map(PathBuf::from)
        .find(|path| path.exists())
        .expect("true executable");
    let daemon = SchedulerDaemon::new(store.clone(), executable);

    let executed = daemon.run_due_tasks_once().await.expect("run");
    assert_eq!(executed, 1);

    let record = store
        .load_record(&summary.id)
        .expect("load")
        .expect("record");
    assert!(record.runtime.last_run_at.is_some());
    assert!(record.runtime.next_run_at.is_none());
    assert_eq!(
        record.runtime.last_status.as_ref().map(ToString::to_string),
        Some("success".to_string())
    );
    assert!(record.runtime.last_artifact_dir.is_some());
}

#[cfg(unix)]
#[tokio::test]
async fn scheduler_daemon_records_prompt_spawn_failures() {
    let temp = tempdir().expect("tempdir");
    let missing_workspace = temp.path().join("missing-workspace");
    let store = DurableTaskStore::with_paths(SchedulerPaths {
        config_root: temp.path().join("cfg"),
        data_root: temp.path().join("data"),
    });
    let now = Utc::now();
    let definition = ScheduledTaskDefinition::new(
        Some("broken".to_string()),
        ScheduleSpec::OneShot(OneShot {
            at: now - ChronoDuration::minutes(1),
        }),
        ScheduledTaskAction::Prompt {
            prompt: "say hello".to_string(),
        },
        Some(missing_workspace),
        now - ChronoDuration::minutes(2),
        None,
    )
    .expect("definition");
    let summary = store.create(definition).expect("create");
    let executable = ["/usr/bin/true", "/bin/true"]
        .into_iter()
        .map(PathBuf::from)
        .find(|path| path.exists())
        .expect("true executable");
    let daemon = SchedulerDaemon::new(store.clone(), executable);

    let executed = daemon.run_due_tasks_once().await.expect("run");
    assert_eq!(executed, 1);

    let record = store
        .load_record(&summary.id)
        .expect("load")
        .expect("record");
    assert!(record.runtime.last_run_at.is_some());
    assert!(record.runtime.next_run_at.is_none());
    assert!(matches!(
        record.runtime.last_status,
        Some(TaskRunStatus::Failed { .. })
    ));
}

#[test]
fn durable_task_overdue_detection_requires_due_unrun_task() {
    let now = utc(2026, 3, 29, 0, 30, 0);
    assert!(durable_task_is_overdue(
        Some(utc(2026, 3, 29, 0, 22, 47)),
        None,
        false,
        now
    ));
    assert!(!durable_task_is_overdue(
        Some(utc(2026, 3, 29, 0, 40, 0)),
        None,
        false,
        now
    ));
    assert!(!durable_task_is_overdue(
        Some(utc(2026, 3, 29, 0, 22, 47)),
        Some(utc(2026, 3, 29, 0, 22, 47)),
        false,
        now
    ));
    assert!(!durable_task_is_overdue(
        Some(utc(2026, 3, 29, 0, 22, 47)),
        None,
        true,
        now
    ));
}

#[test]
fn service_rendering_mentions_schedule_serve() {
    let launchd = render_launchd_plist(Path::new("/usr/local/bin/vtcode"));
    assert!(launchd.contains("schedule"));
    assert!(launchd.contains("serve"));
    let systemd = render_systemd_unit(Path::new("/usr/local/bin/vtcode"));
    assert!(systemd.contains("schedule serve"));
}

#[test]
fn schedule_create_arg_parser_supports_workspace() {
    let parsed = parse_schedule_create_args(
        r#"--prompt "check build" --every 15m --workspace /tmp/demo --name "Build watch""#,
    )
    .expect("parse");
    assert_eq!(parsed.name.as_deref(), Some("Build watch"));
    assert_eq!(parsed.prompt.as_deref(), Some("check build"));
    assert_eq!(parsed.every.as_deref(), Some("15m"));
    assert_eq!(parsed.workspace, Some(PathBuf::from("/tmp/demo")));
}
