//! Rewrite-to-handler alignment tests.
//!
//! Two verification modes live in this file:
//!
//! **Pair verification** (most tests): `skim rewrite <cmd>` is asserted to
//! produce a string containing the expected handler name, and `skim <handler>`
//! is independently invoked with a fixture.  The two steps are independent —
//! the handler invocation is hardcoded, not derived from the rewrite output.
//! This proves (a) the rewrite rule emits a plausible string and (b) the
//! handler parses the fixture, but does NOT prove the rewrite output is itself
//! a valid invocation.
//!
//! **True round-trip** (`test_alignment_cargo_test_true_roundtrip`): step 1's
//! stdout is captured, whitespace-split into tokens, and used verbatim as the
//! step 2 invocation.  This proves the rewrite output is executable and catches
//! typos in the rewrite rule table (e.g. `skim tset cargo` would fail here
//! even though pair-verification tests would remain green).
//!
//! Together the tests verify that:
//!
//! 1. The rewrite rule maps to a subcommand that actually exists.
//! 2. The handler parses the fixture and emits a non-empty compressed summary.
//! 3. The compressed summary is smaller than or equal to the original fixture.
//! 4. (Round-trip only) The rewrite output is a valid executable invocation.
//!
//! This test file complements `cli_e2e_rewrite.rs` (which only checks the
//! rewrite string) and `cli_e2e_exit_codes.rs` (which only checks exit codes).
//!
//! # Alignment table
//!
//! | Developer command        | Rewrite target          | Handler stdin support |
//! |--------------------------|-------------------------|-----------------------|
//! | cargo test               | skim cargo test         | yes (JSON/text)       |
//! | pytest                   | skim pytest             | yes (text)            |
//! | npx vitest               | skim vitest             | yes (text)            |
//! | cargo build              | skim cargo build        | no (runs real cmd)    |
//! | eslint .                 | skim eslint .           | yes (JSON/text)       |
//! | ruff check .             | skim ruff .             | yes (JSON/text)       |
//! | mypy .                   | skim mypy .             | yes (JSON/text)       |
//! | golangci-lint run ./...  | skim golangci ./...     | yes (JSON/text)       |
//! | npm audit                | skim npm audit          | yes (JSON)            |
//! | npm install express      | skim npm install        | yes (JSON/text)       |
//! | pip install flask        | skim pip install        | yes (text)            |
//! | cargo audit              | skim cargo audit        | yes (JSON)            |

use assert_cmd::Command;
use predicates::prelude::*;

fn skim_cmd() -> Command {
    let mut cmd = Command::cargo_bin("skim").unwrap();
    cmd.env_remove("SKIM_PASSTHROUGH");
    cmd
}

// ============================================================================
// Rewrite-to-handler alignment: test handlers
// ============================================================================

/// Verify `cargo test` rewrites to `skim cargo test` AND the handler processes
/// cargo JSON test output correctly.
///
/// v2.8.0: Flat dispatch — `skim cargo test` replaces `skim test cargo`.
#[test]
fn test_alignment_cargo_test_rewrite_and_handler() {
    // Step 1: rewrite produces the expected target.
    skim_cmd()
        .args(["rewrite", "cargo", "test"])
        .assert()
        .success()
        .stdout(predicate::str::contains("skim cargo test"));

    // Step 2: handler accepts fixture stdin and compresses it.
    let fixture = include_str!("fixtures/cmd/test/cargo_pass.json");
    skim_cmd()
        .args(["cargo", "test"])
        .write_stdin(fixture)
        .assert()
        .success()
        .stdout(predicate::str::contains("pass:"));
}

/// True round-trip: capture `skim rewrite cargo test` stdout, split into
/// tokens, execute those tokens as the handler command.
///
/// This is the only test in this file that actually feeds step-1 output into
/// step-2 as the command line.  It proves the rewrite output is a valid,
/// executable handler invocation — a typo like `skim tset cargo` would be
/// caught here even though pair-verification tests would remain green.
///
/// Other alignment tests use the "pair verification" pattern (step 2 hardcodes
/// the handler name) because that is sufficient for testing the handler's
/// parsing behaviour.  Migrating all 12 to true round-trips is tracked as
/// follow-up work.
#[test]
fn test_alignment_cargo_test_true_roundtrip() {
    // Step 1: capture the rewrite output.
    let rewrite_out = skim_cmd()
        .args(["rewrite", "cargo", "test"])
        .output()
        .unwrap();
    assert!(
        rewrite_out.status.success(),
        "skim rewrite cargo test must succeed; got: {:?}",
        rewrite_out.status
    );

    let rewritten = String::from_utf8(rewrite_out.stdout).unwrap();
    let rewritten = rewritten.trim();
    assert!(
        !rewritten.is_empty(),
        "skim rewrite cargo test must emit a non-empty command string"
    );

    // Step 2: parse the rewrite output into tokens and execute.
    // `rewritten` is e.g. "skim cargo test" — split by whitespace and
    // drop the leading "skim" token (assert_cmd already selects the binary).
    // v2.8.0: Flat dispatch — `skim cargo test` replaces `skim test cargo`.
    let tokens: Vec<&str> = rewritten.split_whitespace().collect();
    assert!(
        tokens.first() == Some(&"skim"),
        "rewrite output must start with 'skim'; got: {rewritten}"
    );
    let handler_args = &tokens[1..]; // ["cargo", "test"]

    let fixture = include_str!("fixtures/cmd/test/cargo_pass.json");
    skim_cmd()
        .args(handler_args)
        .write_stdin(fixture)
        .assert()
        .success()
        .stdout(predicate::str::contains("pass:"));
}

/// Verify `pytest` rewrites to `skim pytest` AND the handler processes
/// pytest text output correctly.
///
/// v2.8.0: Flat dispatch — `skim pytest` replaces `skim test pytest`.
#[test]
fn test_alignment_pytest_rewrite_and_handler() {
    skim_cmd()
        .args(["rewrite", "pytest"])
        .assert()
        .success()
        .stdout(predicate::str::contains("skim pytest"));

    let fixture = include_str!("fixtures/cmd/test/pytest_pass.txt");
    skim_cmd()
        .args(["pytest"])
        .write_stdin(fixture)
        .assert()
        .success()
        .stdout(predicate::str::contains("pass:"));
}

/// Verify `npx vitest` rewrites to `skim vitest` AND the handler
/// processes vitest text output.
///
/// v2.8.0: Flat dispatch — `skim vitest` replaces `skim test vitest`.
#[test]
fn test_alignment_npx_vitest_rewrite_and_handler() {
    skim_cmd()
        .args(["rewrite", "npx", "vitest"])
        .assert()
        .success()
        .stdout(predicate::str::contains("skim vitest"));

    // vitest handler accepts stdin. Use a fixture matching the PIPE_RE pattern:
    // "Tests  N passed | N failed | N total" so the handler parses it as Full/Degraded
    // and exits 0 (no failures).
    let fixture = include_str!("fixtures/cmd/test/vitest_regex_fail.txt");
    skim_cmd()
        .args(["vitest"])
        .write_stdin(fixture)
        .assert()
        // vitest exits 1 when there are failures; fixture has 1 failure.
        .code(1)
        .stdout(predicate::str::contains("fail: 1"));
}

// ============================================================================
// Rewrite-to-handler alignment: lint handlers
// ============================================================================

/// Verify `eslint .` rewrites to `skim eslint .` AND the handler
/// processes eslint JSON output.
#[test]
fn test_alignment_eslint_rewrite_and_handler() {
    skim_cmd()
        .args(["rewrite", "eslint", "."])
        .assert()
        .success()
        .stdout(predicate::str::contains("skim eslint"));

    let fixture = include_str!("fixtures/cmd/lint/eslint_fail.json");
    skim_cmd()
        .args(["eslint"])
        .write_stdin(fixture)
        .assert()
        // Non-zero exit on lint failures; just check output contains summary.
        .stdout(predicate::str::contains("eslint"));
}

/// Verify `ruff check .` rewrites to `skim ruff .` AND the handler
/// processes ruff JSON output.
#[test]
fn test_alignment_ruff_rewrite_and_handler() {
    skim_cmd()
        .args(["rewrite", "ruff", "check", "."])
        .assert()
        .success()
        .stdout(predicate::str::contains("skim ruff"));

    let fixture = include_str!("fixtures/cmd/lint/ruff_fail.json");
    skim_cmd()
        .args(["ruff"])
        .write_stdin(fixture)
        .assert()
        .stdout(predicate::str::contains("ruff"));
}

/// Verify `mypy .` rewrites to `skim mypy .` AND the handler processes
/// mypy JSON output.
#[test]
fn test_alignment_mypy_rewrite_and_handler() {
    skim_cmd()
        .args(["rewrite", "mypy", "."])
        .assert()
        .success()
        .stdout(predicate::str::contains("skim mypy"));

    let fixture = include_str!("fixtures/cmd/lint/mypy_fail.json");
    skim_cmd()
        .args(["mypy"])
        .write_stdin(fixture)
        .assert()
        .stdout(predicate::str::contains("mypy"));
}

/// Verify `golangci-lint run ./...` rewrites to `skim golangci ./...`
/// AND the handler processes golangci JSON output.
#[test]
fn test_alignment_golangci_rewrite_and_handler() {
    skim_cmd()
        .args(["rewrite", "golangci-lint", "run", "./..."])
        .assert()
        .success()
        .stdout(predicate::str::contains("skim golangci"));

    let fixture = include_str!("fixtures/cmd/lint/golangci_fail.json");
    skim_cmd()
        .args(["golangci"])
        .write_stdin(fixture)
        .assert()
        .stdout(predicate::str::contains("golangci"));
}

// ============================================================================
// Rewrite-to-handler alignment: pkg handlers
// ============================================================================

/// Verify `npm audit` rewrites to `skim npm audit` AND the handler
/// processes npm audit JSON output.
///
/// v2.8.0: Flat dispatch — `skim npm audit` replaces `skim pkg npm audit`.
#[test]
fn test_alignment_npm_audit_rewrite_and_handler() {
    skim_cmd()
        .args(["rewrite", "npm", "audit"])
        .assert()
        .success()
        .stdout(predicate::str::contains("skim npm audit"));

    let fixture = include_str!("fixtures/cmd/pkg/npm_audit.json");
    skim_cmd()
        .args(["npm", "audit"])
        .write_stdin(fixture)
        .assert()
        .stdout(predicate::str::contains("audit"));
}

/// Verify `npm install express` rewrites to `skim npm install express`
/// AND the handler processes npm install JSON output.
///
/// v2.8.0: Flat dispatch — `skim npm install` replaces `skim pkg npm install`.
#[test]
fn test_alignment_npm_install_rewrite_and_handler() {
    skim_cmd()
        .args(["rewrite", "npm", "install", "express"])
        .assert()
        .success()
        .stdout(predicate::str::contains("skim npm install express"));

    let fixture = include_str!("fixtures/cmd/pkg/npm_install.json");
    skim_cmd()
        .args(["npm", "install"])
        .write_stdin(fixture)
        .assert()
        .stdout(predicate::str::contains("INSTALL").or(predicate::str::contains("install")));
}

/// Verify `cargo audit` rewrites to `skim cargo audit` AND the handler
/// processes cargo audit JSON output.
///
/// v2.8.0: Flat dispatch — `skim cargo audit` replaces `skim pkg cargo audit`.
#[test]
fn test_alignment_cargo_audit_rewrite_and_handler() {
    skim_cmd()
        .args(["rewrite", "cargo", "audit"])
        .assert()
        .success()
        .stdout(predicate::str::contains("skim cargo audit"));

    let fixture = include_str!("fixtures/cmd/pkg/cargo_audit.json");
    skim_cmd()
        .args(["cargo", "audit"])
        .write_stdin(fixture)
        .assert()
        .stdout(predicate::str::contains("audit"));
}

/// Verify `pip install flask` rewrites to `skim pip install flask`
/// AND the handler processes pip install text output.
///
/// v2.8.0: Flat dispatch — `skim pip install` replaces `skim pkg pip install`.
#[test]
fn test_alignment_pip_install_rewrite_and_handler() {
    skim_cmd()
        .args(["rewrite", "pip", "install", "flask"])
        .assert()
        .success()
        .stdout(predicate::str::contains("skim pip install flask"));

    let fixture = include_str!("fixtures/cmd/pkg/pip_install.txt");
    skim_cmd()
        .args(["pip", "install"])
        .write_stdin(fixture)
        .assert()
        .stdout(predicate::str::contains("INSTALL").or(predicate::str::contains("install")));
}

// ============================================================================
// Rewrite-to-handler alignment: ACK (already-compact) commands
// ============================================================================

/// AD-RW-11: `prettier --check` is ACKed — the rewrite echoes the original command
/// (exit 0) rather than mapping to a handler. Verify this does NOT produce a
/// `skim prettier` string (which would imply a handler invocation).
#[test]
fn test_alignment_prettier_check_acked_not_rewritten_to_handler() {
    let output = skim_cmd()
        .args(["rewrite", "prettier", "--check", "."])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "ACKed command must exit 0, got: {:?}",
        output.status
    );
    let stdout = String::from_utf8(output.stdout).unwrap();
    // ACK echoes the original command, NOT a skim subcommand.
    assert!(
        stdout.contains("prettier --check"),
        "ACK must echo original command: {stdout}"
    );
    assert!(
        !stdout.contains("skim prettier"),
        "ACK must NOT produce a handler rewrite: {stdout}"
    );
}

/// AD-RW-11: `rustfmt --check` is ACKed — same semantics as prettier above.
#[test]
fn test_alignment_rustfmt_check_acked_not_rewritten_to_handler() {
    let output = skim_cmd()
        .args(["rewrite", "rustfmt", "--check", "src/main.rs"])
        .output()
        .unwrap();

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(
        stdout.contains("rustfmt --check"),
        "ACK must echo original command: {stdout}"
    );
    assert!(
        !stdout.contains("skim rustfmt"),
        "ACK must NOT produce a handler rewrite: {stdout}"
    );
}

/// AD-RW-11: `cargo fmt --check` is ACKed — added in the evaluator follow-up.
#[test]
fn test_alignment_cargo_fmt_check_acked() {
    let output = skim_cmd()
        .args(["rewrite", "cargo", "fmt", "--check"])
        .output()
        .unwrap();

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(
        stdout.contains("cargo fmt --check"),
        "ACK must echo original command: {stdout}"
    );
    assert!(
        !stdout.contains("skim rustfmt"),
        "ACK must NOT rewrite to a handler: {stdout}"
    );
}

/// AD-RW-11: `cargo fmt -- --check` is ACKed — pass-through variant.
#[test]
fn test_alignment_cargo_fmt_dashdash_check_acked() {
    let output = skim_cmd()
        .args(["rewrite", "cargo", "fmt", "--", "--check"])
        .output()
        .unwrap();

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(
        stdout.contains("cargo fmt -- --check"),
        "ACK must echo original command: {stdout}"
    );
}

/// AD-RW-13 (tokenization): `skim rewrite '<full command>'` with a single quoted
/// positional arg must tokenize by whitespace the same way stdin input does.
///
/// Regression: previously, positional args were passed through as single
/// tokens, so `skim rewrite 'prettier --check src/'` became one token
/// `"prettier --check src/"` which matched no rule and no ACK prefix,
/// silently returning exit 1 with empty stdout.
#[test]
fn test_alignment_rewrite_single_quoted_string_tokenizes_correctly() {
    // Single arg form must produce the same result as multi-arg form.
    let output = skim_cmd()
        .args(["rewrite", "prettier --check src/"])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "single quoted arg must succeed, got: {:?}",
        output.status
    );
    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(
        stdout.contains("prettier --check"),
        "single quoted arg must ACK-echo the command: {stdout}"
    );
}

/// AD-RW-13 (tokenization): same test for `cargo fmt --check` in single-arg form.
#[test]
fn test_alignment_rewrite_single_quoted_cargo_fmt() {
    let output = skim_cmd()
        .args(["rewrite", "cargo fmt --check"])
        .output()
        .unwrap();

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(
        stdout.contains("cargo fmt --check"),
        "single quoted cargo fmt --check must ACK-echo: {stdout}"
    );
}

// ============================================================================
// Handler dispatch alignment: verify subcommand routing
// ============================================================================

/// The rewrite engine and the handler both must agree on the subcommand path.
/// This test exercises `skim cargo test` vs `skim cargo test --` (with sep)
/// to verify the separator is preserved by the rewrite engine.
///
/// v2.8.0: Flat dispatch — `skim cargo test` replaces `skim test cargo`.
#[test]
fn test_alignment_cargo_test_separator_preserved() {
    // Rewrite must preserve the `-- --nocapture` suffix.
    skim_cmd()
        .args(["rewrite", "cargo", "test", "--", "--nocapture"])
        .assert()
        .success()
        .stdout(predicate::str::contains("skim cargo test -- --nocapture"));
}

/// Verify that `npm ci` rewrites to `skim npm install` (alias handling)
/// and the install handler works with npm install JSON fixture.
///
/// v2.8.0: Flat dispatch — `skim npm install` replaces `skim pkg npm install`.
#[test]
fn test_alignment_npm_ci_alias_and_handler() {
    // `npm ci` is an alias for `npm install` in the rewrite rules.
    skim_cmd()
        .args(["rewrite", "npm", "ci"])
        .assert()
        .success()
        .stdout(predicate::str::contains("skim npm install"));

    // The handler it routes to is `skim npm install`.
    let fixture = include_str!("fixtures/cmd/pkg/npm_install.json");
    skim_cmd()
        .args(["npm", "install"])
        .write_stdin(fixture)
        .assert()
        .stdout(predicate::str::contains("INSTALL").or(predicate::str::contains("install")));
}

/// Verify that `python3 -m pytest` rewrites to `skim pytest` and the
/// pytest handler accepts the same fixture as bare `pytest`.
///
/// v2.8.0: Flat dispatch — `skim pytest` replaces `skim test pytest`.
#[test]
fn test_alignment_python3_m_pytest_same_handler_as_bare_pytest() {
    // Both forms rewrite to the same handler.
    let bare = skim_cmd()
        .args(["rewrite", "pytest", "-v"])
        .output()
        .unwrap();
    let python3 = skim_cmd()
        .args(["rewrite", "python3", "-m", "pytest", "-v"])
        .output()
        .unwrap();

    assert!(bare.status.success());
    assert!(python3.status.success());

    let bare_stdout = String::from_utf8(bare.stdout).unwrap();
    let python3_stdout = String::from_utf8(python3.stdout).unwrap();

    // Both must produce the same handler target (skim pytest).
    assert!(
        bare_stdout.contains("skim pytest"),
        "bare pytest must rewrite to skim pytest: {bare_stdout}"
    );
    assert!(
        python3_stdout.contains("skim pytest"),
        "python3 -m pytest must rewrite to skim pytest: {python3_stdout}"
    );
}
