use nils_test_support::bin;
use nils_test_support::cmd::{self, CmdOptions, CmdOutput};
use nils_test_support::http::{HttpResponse, LoopbackServer};
use pretty_assertions::assert_eq;
use serde_json::Value;
use std::fs::OpenOptions;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};

fn claude_cli_bin() -> PathBuf {
    bin::resolve("claude-cli")
}

fn run(args: &[&str], options: &CmdOptions) -> CmdOutput {
    let bin = claude_cli_bin();
    cmd::run_with(&bin, args, options)
}

fn assert_exit(output: &CmdOutput, code: i32) {
    assert_eq!(output.code, code, "stderr: {}", output.stderr_text());
}

fn stdout(output: &CmdOutput) -> String {
    output.stdout_text()
}

fn stderr(output: &CmdOutput) -> String {
    output.stderr_text()
}

fn base_options(cache_dir: &Path) -> CmdOptions {
    CmdOptions::default()
        .with_env_remove_prefix("CLAUDE_PROMPT")
        .with_env_remove("NO_COLOR")
        .with_env_remove("TZ")
        .with_env("CLAUDE_PROMPT_SEGMENT_CACHE_DIR", &path_str(cache_dir))
        .with_env("CLAUDE_PROMPT_SEGMENT_KEYCHAIN_DISABLED", "1")
}

fn path_str(path: &Path) -> String {
    path.to_string_lossy().to_string()
}

fn write_cache(cache_dir: &Path, body: &str) -> PathBuf {
    std::fs::create_dir_all(cache_dir).expect("cache dir");
    let path = cache_dir.join("usage.json");
    std::fs::write(&path, body).expect("write cache");
    path
}

fn make_old(path: &Path) {
    set_modified(path, SystemTime::now() - Duration::from_secs(120));
}

fn set_modified(path: &Path, time: SystemTime) {
    let file = OpenOptions::new().write(true).open(path).expect("open");
    file.set_modified(time).expect("set modified");
}

fn usage_json(five_utilization: f64, weekly_utilization: f64) -> String {
    usage_json_with_resets(
        five_utilization,
        weekly_utilization,
        "2026-01-01T00:00:00+00:00",
        "2026-01-03T12:30:00+00:00",
    )
}

fn usage_json_with_resets(
    five_utilization: f64,
    weekly_utilization: f64,
    five_resets_at: &str,
    weekly_resets_at: &str,
) -> String {
    format!(
        r#"{{
          "usage": {{
            "five_hour": {{"utilization": {five_utilization}, "resets_at": {five_resets_at:?}}},
            "seven_day": {{"utilization": {weekly_utilization}, "resets_at": {weekly_resets_at:?}}}
          }}
        }}"#
    )
}

#[cfg(unix)]
fn write_fake_claude(dir: &Path, body: &str) -> PathBuf {
    let bin_dir = dir.join("bin");
    std::fs::create_dir_all(&bin_dir).expect("fake bin dir");
    let path = bin_dir.join("claude");
    std::fs::write(&path, body).expect("write fake claude");
    let mut permissions = std::fs::metadata(&path).expect("metadata").permissions();
    permissions.set_mode(0o755);
    std::fs::set_permissions(&path, permissions).expect("chmod fake claude");
    bin_dir
}

#[test]
fn main_no_args_prints_help_and_exits_zero() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let output = run(&[], &base_options(tmp.path()));
    assert_exit(&output, 0);
    let help = stdout(&output);
    assert!(help.contains("claude-cli"));
    assert!(help.contains("Prompt-segment command group"));
    assert!(help.contains("completion"));
}

#[test]
fn main_unknown_command_exits_64() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let output = run(&["not-a-real-command"], &base_options(tmp.path()));
    assert_exit(&output, 64);
    assert!(stderr(&output).contains("unrecognized subcommand"));
}

#[test]
fn main_completion_exports_bash_and_zsh_scripts() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let options = base_options(tmp.path());

    let zsh = run(&["completion", "zsh"], &options);
    assert_exit(&zsh, 0);
    let zsh_text = stdout(&zsh);
    assert!(zsh_text.contains("#compdef claude-cli"));
    assert!(zsh_text.contains("prompt-segment:Prompt-segment command group"));
    assert!(zsh_text.contains(":shell -- Shell to generate completion script for:(bash zsh)"));

    let bash = run(&["completion", "bash"], &options);
    assert_exit(&bash, 0);
    let bash_text = stdout(&bash);
    assert!(bash_text.contains("_claude-cli()"));
    assert!(bash_text.contains("complete -F _claude-cli"));
    assert!(bash_text.contains("opts=\"-h --help bash zsh\""));
}

#[cfg(unix)]
#[test]
fn usage_auto_falls_back_to_claude_cli_and_writes_cache() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let bin_dir = write_fake_claude(
        tmp.path(),
        r#"#!/usr/bin/env sh
cat >/dev/null
cat <<'OUT'
Claude Code Usage

Current session
5-hour limit: 25% used, 75% remaining
Resets at 2026-01-01T00:00:00+00:00

Current week
Weekly limit: 50% used, 50% remaining
Resets at 2026-01-03T12:30:00+00:00
OUT
"#,
    );
    let server = LoopbackServer::new().expect("server");
    server.add_route("GET", "/usage", HttpResponse::new(500, "upstream exploded"));

    let output = run(
        &["usage", "--format", "json", "--source", "auto"],
        &base_options(tmp.path())
            .with_path_prepend(&bin_dir)
            .with_env("CLAUDE_PROMPT_SEGMENT_ACCESS_TOKEN", "secret-token-usage")
            .with_env(
                "CLAUDE_PROMPT_SEGMENT_ENDPOINT",
                &format!("{}/usage", server.url()),
            )
            .with_env("CLAUDE_PROMPT_SEGMENT_CLAUDE_PTY_DISABLED", "1"),
    );

    assert_exit(&output, 0);
    assert!(!stdout(&output).contains("secret-token-usage"));
    assert!(!stderr(&output).contains("secret-token-usage"));

    let payload: Value = serde_json::from_str(&stdout(&output)).expect("json");
    assert_eq!(payload["schema_version"], "claude-cli.usage.v1");
    assert_eq!(payload["command"], "usage");
    assert_eq!(payload["ok"], true);
    assert_eq!(payload["result"]["provider"], "claude");
    assert_eq!(payload["result"]["source"], "cli");
    assert_eq!(payload["result"]["stale"], false);
    assert_eq!(payload["result"]["windows"][0]["key"], "5h");
    assert_eq!(payload["result"]["windows"][0]["used_percent"], 25.0);
    assert_eq!(payload["result"]["windows"][0]["remaining_percent"], 75.0);
    assert_eq!(payload["result"]["windows"][1]["key"], "weekly");
    assert_eq!(payload["result"]["windows"][1]["used_percent"], 50.0);
    assert_eq!(payload["result"]["windows"][1]["remaining_percent"], 50.0);

    let cached = std::fs::read_to_string(tmp.path().join("usage.json")).expect("cache");
    assert!(!cached.contains("secret-token-usage"));
    assert!(cached.contains("\"five_hour\""));
    assert!(cached.contains("\"seven_day\""));
}

#[test]
fn usage_cache_source_outputs_epoch_for_human_reset_times() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let cache_file = write_cache(
        tmp.path(),
        &usage_json_with_resets(
            21.0,
            10.0,
            "3:20pm(Asia/Taipei)",
            "Jul 12, 9pm (Asia/Taipei)",
        ),
    );
    set_modified(&cache_file, UNIX_EPOCH + Duration::from_secs(1_783_666_800));

    let output = run(
        &["usage", "--format", "json", "--source", "cache"],
        &base_options(tmp.path()),
    );

    assert_exit(&output, 0);
    let payload: Value = serde_json::from_str(&stdout(&output)).expect("json");
    assert_eq!(payload["schema_version"], "claude-cli.usage.v1");
    assert_eq!(payload["command"], "usage");
    assert_eq!(payload["ok"], true);
    assert_eq!(payload["result"]["updated_at"], 1_783_666_800);
    assert_eq!(
        payload["result"]["windows"][0]["resets_at"],
        "3:20pm(Asia/Taipei)"
    );
    assert_eq!(
        payload["result"]["windows"][0]["resets_at_epoch"],
        1_783_668_000
    );
    assert_eq!(
        payload["result"]["windows"][1]["resets_at"],
        "Jul 12, 9pm (Asia/Taipei)"
    );
    assert_eq!(
        payload["result"]["windows"][1]["resets_at_epoch"],
        1_783_861_200
    );
}

#[test]
fn prompt_segment_check_reads_credentials_json_without_printing_token() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let token = "secret-token-for-check";
    let options = base_options(tmp.path()).with_env(
        "CLAUDE_PROMPT_SEGMENT_CREDENTIALS_JSON",
        &format!(r#"{{"claudeAiOauth":{{"accessToken":"{token}"}}}}"#),
    );

    let output = run(&["prompt-segment", "check"], &options);
    assert_exit(&output, 0);
    assert_eq!(stdout(&output), "");
    assert_eq!(stderr(&output), "");
}

#[test]
fn prompt_segment_is_enabled_returns_one_when_credentials_are_missing() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let output = run(
        &["prompt-segment", "--is-enabled"],
        &base_options(tmp.path()),
    );
    assert_exit(&output, 1);
    assert_eq!(stdout(&output), "");
}

#[test]
fn prompt_segment_renders_fresh_cached_usage_without_credentials() {
    let tmp = tempfile::tempdir().expect("tempdir");
    write_cache(tmp.path(), &usage_json(23.2, 44.1));

    let options = base_options(tmp.path())
        .with_env("NO_COLOR", "1")
        .with_env("TZ", "UTC");
    let output = run(
        &["prompt-segment", "--ttl", "1h", "--time-format", "%Y-%m-%d"],
        &options,
    );

    assert_exit(&output, 0);
    assert_eq!(stdout(&output), "5h:77% W:56% 2026-01-03\n");
    assert_eq!(stderr(&output), "");
}

#[test]
fn prompt_segment_refresh_fetches_and_writes_cache_without_secret_leakage() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let server = LoopbackServer::new().expect("server");
    let body = usage_json(25.0, 50.0);
    server.add_route(
        "GET",
        "/usage",
        HttpResponse::new(200, body.clone()).with_header("Content-Type", "application/json"),
    );

    let token = "secret-token-refresh";
    let endpoint = format!("{}/usage", server.url());
    let options = base_options(tmp.path())
        .with_env("CLAUDE_PROMPT_SEGMENT_ACCESS_TOKEN", token)
        .with_env("CLAUDE_PROMPT_SEGMENT_ENDPOINT", &endpoint)
        .with_env("NO_COLOR", "1")
        .with_env("TZ", "UTC");

    let output = run(
        &["prompt-segment", "--refresh", "--time-format", "%Y-%m-%d"],
        &options,
    );

    assert_exit(&output, 0);
    assert_eq!(stdout(&output), "5h:75% W:50% 2026-01-03\n");
    assert!(!stdout(&output).contains(token));
    assert!(!stderr(&output).contains(token));
    assert_eq!(
        std::fs::read_to_string(tmp.path().join("usage.json")).expect("cache"),
        body
    );

    let requests = server.take_requests();
    assert_eq!(requests.len(), 1);
    assert_eq!(
        requests[0].header_value("authorization"),
        Some(format!("Bearer {token}"))
    );
    assert_eq!(
        requests[0].header_value("anthropic-beta"),
        Some("oauth-2025-04-20".to_string())
    );
    assert_eq!(
        requests[0].header_value("user-agent"),
        Some("claude-code/2.1.0".to_string())
    );
}

#[test]
fn prompt_segment_stale_cache_fallback_suppresses_fetch_errors() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let cache_file = write_cache(tmp.path(), &usage_json(25.0, 50.0));
    make_old(&cache_file);

    let server = LoopbackServer::new().expect("server");
    server.add_route("GET", "/usage", HttpResponse::new(500, "upstream exploded"));

    let options = base_options(tmp.path())
        .with_env("CLAUDE_PROMPT_SEGMENT_ACCESS_TOKEN", "secret-token-stale")
        .with_env(
            "CLAUDE_PROMPT_SEGMENT_ENDPOINT",
            &format!("{}/usage", server.url()),
        )
        .with_env("NO_COLOR", "1")
        .with_env("TZ", "UTC");

    let output = run(
        &["prompt-segment", "--ttl", "1s", "--time-format", "%Y-%m-%d"],
        &options,
    );

    assert_exit(&output, 0);
    assert_eq!(stdout(&output), "5h:75% W:50% 2026-01-03 (stale)\n");
    assert_eq!(stderr(&output), "");
}

#[test]
fn prompt_segment_missing_credentials_and_cache_is_quiet_success() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let output = run(&["prompt-segment"], &base_options(tmp.path()));
    assert_exit(&output, 0);
    assert_eq!(stdout(&output), "");
    assert_eq!(stderr(&output), "");
}

#[test]
fn prompt_segment_status_json_has_stable_envelope_and_no_secret_leakage() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let token = "secret-token-status";
    let options = base_options(tmp.path()).with_env("CLAUDE_PROMPT_SEGMENT_ACCESS_TOKEN", token);

    let output = run(&["prompt-segment", "status", "--format", "json"], &options);
    assert_exit(&output, 0);
    assert!(!stdout(&output).contains(token));
    assert!(!stderr(&output).contains(token));

    let payload: Value = serde_json::from_str(&stdout(&output)).expect("json");
    assert_eq!(payload["schema_version"], "claude-cli.prompt-segment.v1");
    assert_eq!(payload["command"], "prompt-segment status");
    assert_eq!(payload["ok"], true);
    assert_eq!(payload["result"]["authenticated"], true);
    assert_eq!(payload["result"]["auth_source"], "access-token-env");
    assert_eq!(payload["result"]["cache_exists"], false);
    assert_eq!(payload["result"]["reason"], "cache-missing");
}

fn write_claude_session(config_dir: &Path, project: &str, id: &str, cwd: &Path) {
    let dir = config_dir.join("projects").join(project);
    std::fs::create_dir_all(&dir).expect("project dir");
    let line = format!(
        "{{\"sessionId\":\"{id}\",\"cwd\":\"{}\"}}\n",
        cwd.to_string_lossy()
    );
    std::fs::write(dir.join(format!("{id}.jsonl")), line).expect("transcript");
}

#[test]
fn agent_resume_control_char_id_is_usage_error() {
    let tmp = tempfile::TempDir::new().expect("tmp");
    let options = CmdOptions::default().with_cwd(tmp.path());
    let output = run(&["agent", "resume", "bad\tid"], &options);
    assert_exit(&output, 64);
}

#[test]
fn agent_resume_unknown_id_returns_data_error() {
    let tmp = tempfile::TempDir::new().expect("tmp");
    let config = tmp.path().join("claude-config");
    std::fs::create_dir_all(config.join("projects")).expect("projects");
    let elsewhere = tmp.path().join("elsewhere");
    std::fs::create_dir_all(&elsewhere).expect("elsewhere");

    let options = CmdOptions::default()
        .with_cwd(&elsewhere)
        .with_env("CLAUDE_CONFIG_DIR", &path_str(&config));
    let output = run(&["agent", "resume", "absent-id"], &options);

    assert_exit(&output, 65);
    assert!(stderr(&output).contains("no Claude session history"));
}

#[test]
fn agent_resume_launches_claude_in_recorded_cwd_from_unrelated_dir() {
    let tmp = tempfile::TempDir::new().expect("tmp");
    // A recorded cwd with a space exercises exact working-directory handling.
    let repo = tmp.path().join("recorded repo");
    std::fs::create_dir_all(&repo).expect("repo");
    let config = tmp.path().join("claude-config");
    write_claude_session(&config, "-recorded-repo", "cl-x", &repo);
    let elsewhere = tmp.path().join("elsewhere");
    std::fs::create_dir_all(&elsewhere).expect("elsewhere");

    let out_log = tmp.path().join("claude-out.txt");
    let stub_dir = tmp.path().join("stub");
    std::fs::create_dir_all(&stub_dir).expect("stub dir");
    // The stub drops a marker in its own working directory (proving it launched
    // in the recorded cwd) and records its argv to an absolute log path.
    nils_test_support::write_exe(
        &stub_dir,
        "claude",
        &format!(
            "#!/bin/sh\n: > launched-here\nprintf 'ARG:%s\\n' \"$@\" > '{}'\nexit 5\n",
            out_log.display()
        ),
    );

    let options = CmdOptions::default()
        .with_cwd(&elsewhere)
        .with_env("CLAUDE_CONFIG_DIR", &path_str(&config))
        .with_path_prepend(&stub_dir);
    let output = run(&["agent", "resume", "cl-x"], &options);

    assert_exit(&output, 5);
    assert!(
        repo.join("launched-here").exists(),
        "expected claude to be launched in the recorded cwd"
    );
    let logged = std::fs::read_to_string(&out_log).expect("stub log");
    assert_eq!(
        logged.lines().collect::<Vec<_>>(),
        vec!["ARG:--resume", "ARG:cl-x"],
        "claude must be launched with exactly `--resume <id>`"
    );
}

#[test]
fn agent_resume_cd_override_to_missing_directory_is_runtime_error() {
    let tmp = tempfile::TempDir::new().expect("tmp");
    let missing = tmp.path().join("does-not-exist");
    let options = CmdOptions::default().with_cwd(tmp.path());
    let output = run(
        &["agent", "resume", "any-id", "--cd", &path_str(&missing)],
        &options,
    );

    assert_exit(&output, 1);
    assert!(stderr(&output).contains("not an existing directory"));
}

#[test]
fn agent_resume_truncated_scan_returns_runtime_error() {
    let tmp = tempfile::TempDir::new().expect("tmp");
    let repo = tmp.path().join("repo");
    std::fs::create_dir_all(&repo).expect("repo");
    let config = tmp.path().join("claude-config");
    // Two matching projects plus a one-entry budget forces the bounded scan to
    // truncate before it can decide, without ever launching claude.
    write_claude_session(&config, "-proj-a", "trunc-id", &repo);
    write_claude_session(&config, "-proj-b", "trunc-id", &repo);

    let options = CmdOptions::default()
        .with_cwd(tmp.path())
        .with_env("CLAUDE_CONFIG_DIR", &path_str(&config))
        .with_env("AGENT_SESSION_CLAUDE_RESUME_SCAN_MAX_ENTRIES", "1");
    let output = run(&["agent", "resume", "trunc-id"], &options);

    assert_exit(&output, 1);
    assert!(stderr(&output).contains("truncated"));
}

#[test]
fn agent_resume_cd_override_bypasses_resolution() {
    let tmp = tempfile::TempDir::new().expect("tmp");
    let override_dir = tmp.path().join("override target");
    std::fs::create_dir_all(&override_dir).expect("override dir");
    // Empty project history: automatic resolution would fail with NotFound, so a
    // successful launch proves `--cd` bypassed resolution entirely.
    let config = tmp.path().join("claude-config");
    std::fs::create_dir_all(config.join("projects")).expect("projects");

    let stub_dir = tmp.path().join("stub");
    std::fs::create_dir_all(&stub_dir).expect("stub dir");
    nils_test_support::write_exe(
        &stub_dir,
        "claude",
        "#!/bin/sh\n: > launched-here\nexit 5\n",
    );

    let options = CmdOptions::default()
        .with_cwd(tmp.path())
        .with_env("CLAUDE_CONFIG_DIR", &path_str(&config))
        .with_path_prepend(&stub_dir);
    let output = run(
        &[
            "agent",
            "resume",
            "unresolved-id",
            "--cd",
            &path_str(&override_dir),
        ],
        &options,
    );

    assert_exit(&output, 5);
    assert!(
        override_dir.join("launched-here").exists(),
        "expected claude to be launched in the --cd override directory"
    );
}
