//! Integration tests for `skim pytest` subcommand (#47).
//!
//! v2.8.0: Flat dispatch — `skim pytest` replaces `skim test pytest`.
//!
//! Tests end-to-end CLI behavior: help output, piped fixture parsing,
//! and (optionally) real pytest execution when available.

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

// ============================================================================
// Help and subcommand routing
// ============================================================================

#[test]
fn test_skim_pytest_help() {
    // v2.8.0: `skim pytest --help` — "test" is no longer a subcommand.
    // `skim pytest` with --help and no stdin should attempt to run pytest --help.
    // Since we can't guarantee pytest is installed, we just check that
    // the subcommand routing works (doesn't say "not yet implemented").
    let output = Command::cargo_bin("skim")
        .unwrap()
        .args(["pytest", "--help"])
        .output()
        .unwrap();

    // If pytest is installed, it shows pytest help.
    // If not, we get an error about pytest not being found.
    // Either way, we should NOT see the "not yet implemented" message.
    let combined = format!(
        "{}{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(
        !combined.contains("not yet implemented"),
        "skim pytest should be implemented, got: {combined}"
    );
}

// ============================================================================
// Piped fixture tests
// ============================================================================

#[test]
fn test_piped_all_pass() {
    let fixture = include_str!("fixtures/cmd/test/pytest_pass.txt");
    Command::cargo_bin("skim")
        .unwrap()
        .args(["pytest"])
        .write_stdin(fixture)
        .assert()
        .success()
        .stdout(predicate::str::contains("pass: 5"))
        .stdout(predicate::str::contains("fail: 0"));
}

#[test]
fn test_piped_with_failures() {
    let fixture = include_str!("fixtures/cmd/test/pytest_fail.txt");
    Command::cargo_bin("skim")
        .unwrap()
        .args(["pytest"])
        .write_stdin(fixture)
        .assert()
        .code(predicate::ne(0))
        .stdout(predicate::str::contains("pass: 2"))
        .stdout(predicate::str::contains("fail: 1"))
        .stdout(predicate::str::contains("test_divide").or(predicate::str::contains("test_math")));
}

#[test]
fn test_piped_mixed() {
    let fixture = include_str!("fixtures/cmd/test/pytest_mixed.txt");
    Command::cargo_bin("skim")
        .unwrap()
        .args(["pytest"])
        .write_stdin(fixture)
        .assert()
        .code(predicate::ne(0))
        .stdout(predicate::str::contains("pass: 4"))
        .stdout(predicate::str::contains("fail: 1"))
        .stdout(predicate::str::contains("skip: 1"));
}

#[test]
fn test_piped_all_failures() {
    let fixture = include_str!("fixtures/cmd/test/pytest_all_fail.txt");
    Command::cargo_bin("skim")
        .unwrap()
        .args(["pytest"])
        .write_stdin(fixture)
        .assert()
        .code(predicate::ne(0))
        .stdout(predicate::str::contains("pass: 0"))
        .stdout(predicate::str::contains("fail: 3"));
}

#[test]
fn test_piped_passthrough_for_garbage() {
    Command::cargo_bin("skim")
        .unwrap()
        .args(["pytest"])
        .write_stdin("this is not pytest output\n")
        .assert()
        // Passthrough: should still output something (the raw input)
        .stdout(predicate::str::contains("this is not pytest output"));
}

// ============================================================================
// Passthrough mode (SKIM_PASSTHROUGH=1)
// ============================================================================

/// When SKIM_PASSTHROUGH=1 and input is piped, raw output is forwarded unchanged
/// (no compression header, no PASS:/FAIL: counts).
#[test]
fn test_piped_passthrough_mode_skips_compression() {
    let fixture = include_str!("fixtures/cmd/test/pytest_fail.txt");
    Command::cargo_bin("skim")
        .unwrap()
        .args(["pytest"])
        .env("SKIM_PASSTHROUGH", "1")
        .write_stdin(fixture)
        .assert()
        // Raw pytest output contains "passed" or "failed"
        .stdout(predicate::str::contains("passed").or(predicate::str::contains("failed")))
        // Compressed pass:/fail: counts must not appear
        .stdout(predicate::str::contains("pass:").not())
        .stdout(predicate::str::contains("fail:").not());
}

// ============================================================================
// Failure context
// ============================================================================

/// When pytest reports failures, the compressed output must include a raw
/// failure context tail so the agent can diagnose failures without re-running.
#[test]
fn test_failure_context_appended_on_failures() {
    let fixture = include_str!("fixtures/cmd/test/pytest_fail.txt");
    Command::cargo_bin("skim")
        .unwrap()
        .args(["pytest"])
        .write_stdin(fixture)
        .assert()
        .code(predicate::ne(0))
        .stdout(predicate::str::contains("fail: 1"))
        .stdout(predicate::str::contains("--- failure context"));
}

/// When all tests pass there must be no failure context tail.
#[test]
fn test_failure_context_absent_on_all_pass() {
    let fixture = include_str!("fixtures/cmd/test/pytest_pass.txt");
    Command::cargo_bin("skim")
        .unwrap()
        .args(["pytest"])
        .write_stdin(fixture)
        .assert()
        .success()
        .stdout(predicate::str::contains("--- failure context").not());
}

// ============================================================================
// Unknown runner
// ============================================================================

// v2.8.0: "test" is no longer a subcommand. Unknown tool names are unknown
// subcommands at the dispatch level. This test is removed.

// ============================================================================
// Real pytest execution (gated on pytest availability)
// ============================================================================

#[test]
fn test_real_pytest_if_available() {
    // Skip if pytest is not installed
    if process::Command::new("pytest")
        .arg("--version")
        .output()
        .is_err()
    {
        return;
    }

    // Run a trivial passing test
    let dir = tempfile::TempDir::new().unwrap();
    let test_file = dir.path().join("test_trivial.py");
    std::fs::write(
        &test_file,
        "def test_one():\n    assert 1 + 1 == 2\n\ndef test_two():\n    assert True\n",
    )
    .unwrap();

    let output = Command::cargo_bin("skim")
        .unwrap()
        .args(["pytest", test_file.to_str().unwrap()])
        .output()
        .unwrap();

    let stdout = String::from_utf8_lossy(&output.stdout);

    assert!(
        stdout.contains("pass: 2"),
        "expected pass: 2 from real pytest, got: {stdout}"
    );
    assert!(
        stdout.contains("fail: 0"),
        "expected fail: 0 from real pytest, got: {stdout}"
    );
}

// ============================================================================
// Args prevent stdin mode (regression: subprocess stdin detection bug)
// ============================================================================

#[test]
fn test_pytest_with_args_does_not_read_stdin() {
    // With args present, skim should spawn pytest instead of reading stdin.
    // When pytest is not installed (CI), the spawn error goes to stderr
    // (unlike vitest which has an npx fallback producing stdout). Assert
    // on stderr to prove the spawn path was taken — the "pytest" name in
    // the error message confirms skim attempted to exec it.
    let output = Command::cargo_bin("skim")
        .unwrap()
        .env_remove("SKIM_PASSTHROUGH")
        .env_remove("SKIM_DEBUG")
        .arg("pytest")
        .arg("-v")
        .output()
        .unwrap();
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let has_output = !stdout.trim().is_empty() || stderr.contains("pytest");
    assert!(
        has_output,
        "spawn path should produce output (stdout or pytest error on stderr)"
    );
}
