//! Integration tests for the tidalctl binary.
//!
//! Tests run the actual binary as a subprocess via `std::process::Command`.

#![forbid(unsafe_code)]
#![allow(clippy::unwrap_used)]

use std::process::Command;

use tidaldb::db::temp::TempTidalHome;
use tidaldb::wal::{SignalEvent, WalConfig, WalHandle};

fn tidalctl_bin() -> Command {
    Command::new(env!("CARGO_BIN_EXE_tidalctl"))
}

/// Create a `TempTidalHome` with WAL segments and a checkpoint.
/// Returns the home so it stays alive (not dropped) for the test.
fn home_with_wal_data() -> TempTidalHome {
    let home = TempTidalHome::new().expect("create temp home");
    let paths = home.paths();
    paths.ensure_all().expect("create subdirs");

    let config = WalConfig {
        dir: home.path().to_path_buf(),
        ..WalConfig::default()
    };

    let (handle, _replayed, _session_events) = WalHandle::open(config).expect("open WAL");

    for i in 1..=5 {
        let event = SignalEvent {
            entity_id: i,
            signal_type: 1,
            weight: 1.0,
            timestamp_nanos: i * 1_000_000_000,
        };
        let _seq = handle.append(event).expect("append event");
    }

    handle.checkpoint(3).expect("checkpoint");
    handle.shutdown().expect("shutdown WAL");

    home
}

#[test]
fn status_with_wal_segments_exits_0() {
    let home = home_with_wal_data();

    let output = tidalctl_bin()
        .args(["status", "--path", home.path().to_str().unwrap()])
        .output()
        .expect("run tidalctl");

    assert!(
        output.status.success(),
        "exit code: {}, stderr: {}",
        output.status,
        String::from_utf8_lossy(&output.stderr)
    );

    let stdout = String::from_utf8(output.stdout).expect("utf8");
    let json: serde_json::Value = serde_json::from_str(&stdout).expect("parse JSON");

    assert_eq!(json["status"], "ok");
    assert!(
        json["wal"]["segments"].as_u64().unwrap_or(0) > 0,
        "should have segments: {stdout}"
    );
    assert!(
        json["wal"]["checkpoint_seq"].as_u64().unwrap_or(0) > 0,
        "should have checkpoint: {stdout}"
    );
    assert!(json["version"].is_string());
    assert!(json["build_hash"].is_string());
    assert!(json["dirs"]["base"].is_string());
}

#[test]
fn status_empty_dir_shows_empty() {
    let home = TempTidalHome::new().expect("create temp home");
    let paths = home.paths();
    paths.ensure_all().expect("create subdirs");

    let output = tidalctl_bin()
        .args(["status", "--path", home.path().to_str().unwrap()])
        .output()
        .expect("run tidalctl");

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).expect("utf8");
    let json: serde_json::Value = serde_json::from_str(&stdout).expect("parse JSON");
    assert_eq!(json["status"], "empty");
}

#[test]
fn status_nonexistent_path_exits_0_with_empty() {
    // When the WAL dir doesn't exist, gather_wal_state returns segments=0
    // which maps to "empty" status. The base dir may or may not exist.
    let output = tidalctl_bin()
        .args(["status", "--path", "/nonexistent/path/that/does/not/exist"])
        .output()
        .expect("run tidalctl");

    let stdout = String::from_utf8(output.stdout).expect("utf8");
    // This should still be valid JSON regardless of status
    if output.status.success() {
        let json: serde_json::Value = serde_json::from_str(&stdout).expect("parse JSON");
        assert!(
            json["status"] == "empty" || json["status"] == "error",
            "status should be empty or error: {stdout}"
        );
    } else {
        let stderr = String::from_utf8(output.stderr).expect("utf8");
        let json: serde_json::Value = serde_json::from_str(&stderr).expect("parse error JSON");
        assert!(json["error"].is_string());
    }
}

#[test]
fn paths_exits_0_with_all_dirs() {
    let home = TempTidalHome::new().expect("create temp home");
    let paths = home.paths();
    paths.ensure_all().expect("create subdirs");

    let output = tidalctl_bin()
        .args(["paths", "--path", home.path().to_str().unwrap()])
        .output()
        .expect("run tidalctl");

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).expect("utf8");
    let json: serde_json::Value = serde_json::from_str(&stdout).expect("parse JSON");

    // Check all six directory paths are present
    assert!(json["base"].is_string(), "missing base: {stdout}");
    assert!(json["wal"].is_string(), "missing wal: {stdout}");
    assert!(json["items"].is_string(), "missing items: {stdout}");
    assert!(json["users"].is_string(), "missing users: {stdout}");
    assert!(json["creators"].is_string(), "missing creators: {stdout}");
    assert!(json["cache"].is_string(), "missing cache: {stdout}");

    // Check exists map
    assert!(json["exists"]["base"].is_boolean());
    assert!(json["exists"]["wal"].is_boolean());
    assert_eq!(json["exists"]["base"], true);
    assert_eq!(json["exists"]["wal"], true);
}

#[test]
fn paths_nonexistent_dir_shows_false_exists() {
    let output = tidalctl_bin()
        .args(["paths", "--path", "/nonexistent/does/not/exist"])
        .output()
        .expect("run tidalctl");

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).expect("utf8");
    let json: serde_json::Value = serde_json::from_str(&stdout).expect("parse JSON");

    assert_eq!(json["exists"]["base"], false);
    assert_eq!(json["exists"]["wal"], false);
}

#[test]
fn pretty_flag_produces_indented_json() {
    let home = TempTidalHome::new().expect("create temp home");

    let output = tidalctl_bin()
        .args(["paths", "--path", home.path().to_str().unwrap(), "--pretty"])
        .output()
        .expect("run tidalctl");

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).expect("utf8");
    // Pretty-printed JSON should have newlines and indentation
    assert!(
        stdout.contains('\n'),
        "pretty output should have newlines: {stdout}"
    );
    assert!(
        stdout.contains("  "),
        "pretty output should have indentation: {stdout}"
    );
    // Should still be valid JSON
    let _json: serde_json::Value = serde_json::from_str(&stdout).expect("parse JSON");
}

#[test]
fn bad_command_exits_1() {
    let output = tidalctl_bin()
        .args(["nonexistent_command", "--path", "/tmp"])
        .output()
        .expect("run tidalctl");

    assert!(!output.status.success(), "should exit 1 for bad command");
    let stderr = String::from_utf8(output.stderr).expect("utf8");
    assert!(
        stderr.contains("error"),
        "stderr should contain error: {stderr}"
    );
}

#[test]
fn no_args_exits_1() {
    let output = tidalctl_bin().output().expect("run tidalctl");

    assert!(!output.status.success(), "should exit 1 with no args");
}

#[test]
fn missing_path_flag_exits_1() {
    let output = tidalctl_bin()
        .args(["status"])
        .output()
        .expect("run tidalctl");

    assert!(!output.status.success(), "should exit 1 without --path");
    let stderr = String::from_utf8(output.stderr).expect("utf8");
    assert!(
        stderr.contains("error"),
        "stderr should contain error: {stderr}"
    );
}

// ---------------------------------------------------------------------------
// recover --verify-only tests
// ---------------------------------------------------------------------------

#[test]
fn recover_verify_only_with_wal_data_json() {
    let home = home_with_wal_data();

    let output = tidalctl_bin()
        .args([
            "recover",
            "--verify-only",
            "--path",
            home.path().to_str().unwrap(),
        ])
        .output()
        .expect("run tidalctl");

    assert!(
        output.status.success(),
        "exit code: {}, stderr: {}",
        output.status,
        String::from_utf8_lossy(&output.stderr)
    );

    let stdout = String::from_utf8(output.stdout).expect("utf8");
    let json: serde_json::Value = serde_json::from_str(&stdout).expect("parse JSON");

    assert!(
        json["total_events"].as_u64().unwrap_or(0) > 0,
        "should have total_events: {stdout}"
    );
    assert!(
        json["segment_count"].as_u64().unwrap_or(0) > 0,
        "should have segments: {stdout}"
    );
    assert!(
        json["checkpoint_seq"].as_u64().unwrap_or(0) > 0,
        "should have checkpoint: {stdout}"
    );
    assert!(
        json["estimated_recovery_secs"].as_f64().unwrap_or(0.0) > 0.0,
        "should have recovery time estimate: {stdout}"
    );
    assert!(
        json["segments"].is_array(),
        "should have segments array: {stdout}"
    );
}

#[test]
fn recover_verify_only_empty_dir() {
    let home = TempTidalHome::new().expect("create temp home");
    let paths = home.paths();
    paths.ensure_all().expect("create subdirs");

    let output = tidalctl_bin()
        .args([
            "recover",
            "--verify-only",
            "--path",
            home.path().to_str().unwrap(),
        ])
        .output()
        .expect("run tidalctl");

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).expect("utf8");
    let json: serde_json::Value = serde_json::from_str(&stdout).expect("parse JSON");

    assert_eq!(json["total_events"], 0);
    assert_eq!(json["segment_count"], 0);
    assert_eq!(json["inconsistency_count"], 0);
}

#[test]
fn recover_verify_only_pretty_output() {
    let home = home_with_wal_data();

    let output = tidalctl_bin()
        .args([
            "recover",
            "--verify-only",
            "--path",
            home.path().to_str().unwrap(),
            "--pretty",
        ])
        .output()
        .expect("run tidalctl");

    assert!(
        output.status.success(),
        "exit code: {}, stderr: {}",
        output.status,
        String::from_utf8_lossy(&output.stderr)
    );

    let stdout = String::from_utf8(output.stdout).expect("utf8");
    assert!(
        stdout.contains("WAL Diagnostic Report"),
        "should contain report header: {stdout}"
    );
    assert!(
        stdout.contains("Checkpoint:"),
        "should contain checkpoint section: {stdout}"
    );
    assert!(
        stdout.contains("Events:"),
        "should contain events section: {stdout}"
    );
    assert!(
        stdout.contains("Estimated Recovery Time:"),
        "should contain recovery estimate: {stdout}"
    );
    assert!(
        stdout.contains("Segment Inventory:"),
        "should contain segment table: {stdout}"
    );
}

#[test]
fn recover_without_verify_only_exits_1() {
    let home = TempTidalHome::new().expect("create temp home");

    let output = tidalctl_bin()
        .args(["recover", "--path", home.path().to_str().unwrap()])
        .output()
        .expect("run tidalctl");

    assert!(
        !output.status.success(),
        "should exit 1 without --verify-only"
    );
    let stderr = String::from_utf8(output.stderr).expect("utf8");
    assert!(
        stderr.contains("verify-only"),
        "stderr should mention verify-only: {stderr}"
    );
}

#[test]
fn recover_nonexistent_path_returns_empty_report() {
    let output = tidalctl_bin()
        .args([
            "recover",
            "--verify-only",
            "--path",
            "/nonexistent/path/that/does/not/exist",
        ])
        .output()
        .expect("run tidalctl");

    // Should succeed with an empty report (no WAL dir = empty report).
    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).expect("utf8");
    let json: serde_json::Value = serde_json::from_str(&stdout).expect("parse JSON");
    assert_eq!(json["total_events"], 0);
    assert_eq!(json["segment_count"], 0);
}

// ---------------------------------------------------------------------------
// diagnostics tests
// ---------------------------------------------------------------------------

#[test]
fn diagnostics_json_output_valid() {
    let home = home_with_wal_data();

    let output = tidalctl_bin()
        .args(["diagnostics", "--path", home.path().to_str().unwrap()])
        .output()
        .expect("run tidalctl diagnostics");

    assert!(
        output.status.success(),
        "exit code: {}, stderr: {}",
        output.status.code().unwrap_or(-1),
        String::from_utf8_lossy(&output.stderr)
    );

    let stdout = String::from_utf8(output.stdout).expect("valid utf8");
    let json: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
    assert!(json["version"].is_string());
    assert!(json["wal_segments"].is_number());
    assert!(json["checkpoint_age_seconds"].is_number());
    assert!(json["tantivy_segments"].is_number());
    assert!(json["tantivy_indexed_docs"].is_number());
    assert!(json["checkpoint_wal_sequence"].is_number());
    assert!(json["wal_total_bytes"].is_number());
    assert!(json["usearch_directory_bytes"].is_number());
    assert!(json["degradation_level"].is_number());
    assert!(json["collection_count"].is_number());
    assert!(json["cohort_count"].is_number());
}

#[test]
fn diagnostics_pretty_output_readable() {
    let home = home_with_wal_data();

    let output = tidalctl_bin()
        .args([
            "diagnostics",
            "--path",
            home.path().to_str().unwrap(),
            "--pretty",
        ])
        .output()
        .expect("run tidalctl diagnostics");

    assert!(output.status.success());
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        stdout.contains("tidalDB Diagnostics"),
        "missing header: {stdout}"
    );
    assert!(stdout.contains("WAL"), "missing WAL section: {stdout}");
    assert!(
        stdout.contains("Checkpoint"),
        "missing Checkpoint section: {stdout}"
    );
    assert!(
        stdout.contains("Signal Ledger"),
        "missing Signal Ledger section: {stdout}"
    );
    assert!(
        stdout.contains("Text Index (Tantivy)"),
        "missing Text Index section: {stdout}"
    );
    assert!(
        stdout.contains("Vector Index (USearch)"),
        "missing Vector Index section: {stdout}"
    );
    assert!(
        stdout.contains("Sessions"),
        "missing Sessions section: {stdout}"
    );
    assert!(
        stdout.contains("Degradation"),
        "missing Degradation section: {stdout}"
    );
}

#[test]
fn diagnostics_missing_dir_exits_1() {
    let output = tidalctl_bin()
        .args([
            "diagnostics",
            "--path",
            "/nonexistent/tidaldb/diagnostics/path",
        ])
        .output()
        .expect("run tidalctl diagnostics");
    assert_eq!(output.status.code(), Some(1));
}
