//! Adversarial compression tests based on TheDecipherist/rtk-test methodology.
//! Each test verifies that safety-critical information survives compression.

use lean_ctx::core::patterns::compress_output;

#[test]
fn adversarial_git_diff_preserves_code_content() {
    let diff = "diff --git a/src/auth.rs b/src/auth.rs\n\
                index abc123..def456 100644\n\
                --- a/src/auth.rs\n\
                +++ b/src/auth.rs\n\
                @@ -10,6 +10,8 @@ fn verify_token(token: &str) -> bool {\n\
                     let decoded = decode(token);\n\
                     if decoded.is_err() {\n\
                         return false;\n\
                +    }\n\
                +    if decoded.unwrap().exp < now() {\n\
                +        return false; // expired token\n\
                     }\n\
                     true\n\
                 }";

    let compressed = compress_output("git diff", diff).unwrap();
    assert!(
        compressed.contains("expired token"),
        "diff must preserve code content: {compressed}"
    );
    assert!(
        compressed.contains('+'),
        "diff must preserve +/- markers: {compressed}"
    );
}

#[test]
fn adversarial_git_diff_preserves_security_bug() {
    let diff = "diff --git a/src/api.rs b/src/api.rs\n\
--- a/src/api.rs\n\
+++ b/src/api.rs\n\
@@ -5,3 +5,5 @@\n\
-    verify_csrf_token(&request);\n\
+    // TODO: re-enable CSRF check\n\
+    // verify_csrf_token(&request);\n\
     process_request(&request);\n";

    let compressed = compress_output("git diff", diff).unwrap_or_else(|| diff.to_string());
    assert!(
        compressed.contains("CSRF") || compressed.contains("csrf"),
        "diff must preserve security-relevant changes: {compressed}"
    );
    assert!(
        compressed.contains("verify_csrf_token"),
        "diff must preserve removed function calls: {compressed}"
    );
}

#[test]
fn adversarial_docker_ps_preserves_unhealthy() {
    let ps_output = "CONTAINER ID   IMAGE          COMMAND       CREATED       STATUS                     PORTS     NAMES\n\
                     abc123def456   nginx:latest   \"nginx -g…\"   2 hours ago   Up 2 hours (unhealthy)     80/tcp    web-prod\n\
                     789ghi012jkl   redis:7        \"redis-se…\"   3 hours ago   Up 3 hours (healthy)       6379/tcp  cache-prod\n\
                     345mno678pqr   postgres:16    \"docker-e…\"   5 hours ago   Exited (1) 30 minutes ago            db-prod";

    // docker ps is Verbatim via OutputPolicy — compress_output returns None,
    // meaning output is preserved unmodified. This is the safest outcome.
    assert!(
        compress_output("docker ps", ps_output).is_none(),
        "docker ps must be Verbatim (no compression)"
    );
}

#[test]
fn adversarial_df_preserves_root_filesystem() {
    let mut lines = vec!["Filesystem     1K-blocks    Used Available Use% Mounted on".to_string()];
    lines.push("/dev/sda1  100000000  95000000  5000000  95% /".to_string());
    for i in 0..15 {
        lines.push(format!(
            "tmpfs             1000      100      900   10% /snap/core/{i}"
        ));
    }
    let df_output = lines.join("\n");

    let result = compress_output("df -h", &df_output).unwrap_or_else(|| df_output.clone());
    assert!(
        result.contains("/dev/sda1") || result.contains("95%"),
        "df must preserve root filesystem info: {result}"
    );
    assert!(
        result.contains("/ ") || result.contains("Mounted on"),
        "df must preserve mount points: {result}"
    );
}

#[test]
fn adversarial_pytest_preserves_xfail_xpass() {
    let output = "============================= test session starts ==============================\n\
                  collected 20 items\n\
                  \n\
                  tests/test_auth.py ....x.X...                                          [100%]\n\
                  \n\
                  ================== 15 passed, 2 xfailed, 1 xpassed, 2 warnings in 3.5s ==================";

    let compressed = compress_output("pytest", output).unwrap();
    assert!(
        compressed.contains("xfailed") || compressed.contains("xfail"),
        "pytest must preserve xfailed counter: {compressed}"
    );
    assert!(
        compressed.contains("xpassed") || compressed.contains("xpass"),
        "pytest must preserve xpassed counter: {compressed}"
    );
    assert!(
        compressed.contains("warning"),
        "pytest must preserve warnings counter: {compressed}"
    );
}

#[test]
fn adversarial_git_log_preserves_full_history() {
    let mut log_lines: Vec<String> = Vec::new();
    for i in 0..60 {
        log_lines.push(format!("abc{i:04x} fix: commit message number {i}"));
    }
    let output = log_lines.join("\n");

    let compressed_unlimited = compress_output("git log -n 60 --oneline", &output).unwrap();
    assert!(
        compressed_unlimited.contains("commit message number 59"),
        "git log with explicit -n must preserve all entries: {compressed_unlimited}"
    );

    let compressed_default = compress_output("git log --oneline", &output).unwrap();
    assert!(
        compressed_default.contains("commit message number 59"),
        "git log default should show all 60 entries (under 100 cap): {compressed_default}"
    );
}

#[test]
fn adversarial_grep_preserves_context() {
    let mut grep_lines: Vec<String> = Vec::new();
    for i in 0..80 {
        grep_lines.push(format!("src/auth.rs:{i}:    let user = get_user(id);"));
    }
    let output = grep_lines.join("\n");

    let result = compress_output("grep -rn 'get_user'", &output);
    if let Some(ref compressed) = result {
        assert!(
            compressed.contains("get_user"),
            "grep compressed output must preserve key content: {compressed}"
        );
        assert!(
            compressed.contains("auth.rs"),
            "grep compressed output must preserve file reference: {compressed}"
        );
        assert!(
            compressed.len() <= output.len(),
            "grep compression must not inflate: {} vs {}",
            compressed.len(),
            output.len()
        );
    }
}

#[test]
fn adversarial_log_preserves_critical_severity() {
    let mut log = Vec::new();
    for i in 0..40 {
        log.push(format!("2024-01-01 10:00:{i:02} INFO  request processed"));
    }
    log.insert(
        20,
        "2024-01-01 10:00:20 CRITICAL database connection lost".to_string(),
    );
    log.insert(
        25,
        "2024-01-01 10:00:25 ERROR OOMKilled: container exceeded memory".to_string(),
    );
    let output = log.join("\n");

    let compressed = compress_output("cat /var/log/app.log", &output);
    let text = compressed.unwrap_or_else(|| output.clone());
    assert!(
        text.contains("CRITICAL") || text.contains("database connection lost"),
        "log output must preserve CRITICAL lines: {text}"
    );
}

#[test]
fn adversarial_npm_audit_preserves_cve_ids() {
    let audit = "# npm audit report\n\
                 \n\
                 lodash  <=4.17.20\n\
                 Severity: critical\n\
                 Prototype Pollution - https://github.com/advisories/GHSA-xxxx\n\
                 fix available via `npm audit fix --force`\n\
                 depends on vulnerable versions of lodash\n\
                 node_modules/lodash\n\
                 \n\
                 express  <4.17.3\n\
                 Severity: high\n\
                 CVE-2024-12345 - Open redirect vulnerability\n\
                 fix available via `npm audit fix`\n\
                 node_modules/express\n\
                 \n\
                 2 vulnerabilities (1 high, 1 critical)\n";

    // npm audit is Verbatim via OutputPolicy (is_package_manager_info),
    // so compress_output returns None — output preserved fully.
    assert!(
        compress_output("npm audit", audit).is_none(),
        "npm audit must be Verbatim (no compression)"
    );
}

#[test]
fn adversarial_docker_logs_preserves_critical() {
    let mut log = Vec::new();
    for i in 0..50 {
        log.push(format!(
            "2024-01-01T10:00:{i:02}Z INFO  healthy check passed"
        ));
    }
    log.insert(
        15,
        "2024-01-01T10:00:15Z FATAL  out of memory, container killed".to_string(),
    );
    log.insert(
        30,
        "2024-01-01T10:00:30Z ERROR  panic: runtime error".to_string(),
    );
    let output = log.join("\n");

    // docker logs is Verbatim via OutputPolicy (is_log_viewer),
    // so output is preserved in full — no compression at all.
    assert!(
        compress_output("docker logs mycontainer", &output).is_none(),
        "docker logs must be Verbatim (no compression)"
    );
}

#[test]
fn adversarial_pip_uninstall_preserves_package_names() {
    let output = "Found existing installation: requests 2.28.0\n\
                  Uninstalling requests-2.28.0:\n\
                    Successfully uninstalled requests-2.28.0\n\
                  Found existing installation: flask 2.3.0\n\
                  Uninstalling flask-2.3.0:\n\
                    Successfully uninstalled flask-2.3.0\n\
                  Found existing installation: numpy 1.24.0\n\
                  Uninstalling numpy-1.24.0:\n\
                    Successfully uninstalled numpy-1.24.0\n";

    let compressed = compress_output("pip uninstall requests flask numpy -y", output).unwrap();
    assert!(
        compressed.contains("requests"),
        "pip uninstall must list package names: {compressed}"
    );
    assert!(
        compressed.contains("flask"),
        "pip uninstall must list package names: {compressed}"
    );
    assert!(
        compressed.contains("numpy"),
        "pip uninstall must list package names: {compressed}"
    );
}

#[test]
fn adversarial_middle_truncation_preserves_errors() {
    let mut lines: Vec<String> = Vec::new();
    for i in 0..60 {
        lines.push(format!("line {i}: normal output"));
    }
    lines[30] = "ERROR: critical failure in module X".to_string();
    lines[35] = "WARNING: disk space low".to_string();
    let output = lines.join("\n");

    let compressed = lean_ctx::shell::compress_if_beneficial_pub("unknown-command", &output);
    if compressed.contains('[') && compressed.contains("omitted") {
        assert!(
            compressed.contains("ERROR") || compressed.contains("critical failure"),
            "truncation must preserve error lines: {compressed}"
        );
    }
}

// ===== Regression tests: Scenarios that were SAFE in TheDecipherist/rtk-test v3.2.5 =====
// These must stay SAFE after the adversarial hardening changes.

#[test]
fn regression_git_status_detached_head() {
    let output = "HEAD detached at 48a7098\nnothing to commit, working tree clean";
    let result = compress_output("git status", output).unwrap_or_else(|| output.to_string());
    assert!(
        result.contains("detached") || result.contains("HEAD detached"),
        "git status must preserve DETACHED HEAD warning: {result}"
    );
}

#[test]
fn regression_log_critical_severity() {
    let output = "[INFO] health check ok\n\
                  [INFO] health check ok\n\
                  [CRITICAL] database connection lost\n\
                  [INFO] health check ok\n\
                  [ERROR] retry failed";
    let compressed = compress_output("cat /var/log/app.log", output);
    let text = compressed.unwrap_or_else(|| output.to_string());
    assert!(
        text.contains("CRITICAL"),
        "cat log must preserve CRITICAL lines: {text}"
    );
    assert!(
        text.contains("ERROR"),
        "cat log must preserve ERROR lines: {text}"
    );
}

#[test]
fn regression_ls_shows_dotenv() {
    let output = ".env\n.gitignore\nREADME.md\nsrc\npackage.json";
    let compressed = compress_output("ls -a", output);
    let text = compressed.unwrap_or_else(|| output.to_string());
    assert!(text.contains(".env"), "ls must show .env file: {text}");
}

#[test]
fn regression_pip_list_all_packages() {
    let mut lines = vec![
        "Package    Version".to_string(),
        "---------- -------".to_string(),
    ];
    for i in 0..50 {
        lines.push(format!("package-{i}  1.0.{i}"));
    }
    let output = lines.join("\n");
    let compressed = compress_output("pip list", &output);
    let text = compressed.unwrap_or_else(|| output.clone());
    assert!(
        text.contains("package-0") && text.contains("package-49"),
        "pip list must show all packages: first and last must be present"
    );
}

#[test]
fn regression_git_stash_verbatim() {
    let output = "No local changes to save";
    let compressed = compress_output("git stash", output);
    let text = compressed.unwrap_or_else(|| output.to_string());
    assert!(
        text.contains("No local changes"),
        "git stash must pass through verbatim: {text}"
    );

    let output2 = "Saved working directory and index state WIP on main: abc1234 fix: typo";
    let compressed2 = compress_output("git stash", output2);
    let text2 = compressed2.unwrap_or_else(|| output2.to_string());
    assert!(
        text2.contains("Saved") || text2.contains("WIP on main"),
        "git stash save must pass through: {text2}"
    );
}

#[test]
fn regression_ruff_preserves_file_line_col() {
    let output = "src/api.py:42:10: E501 Line too long (120 > 79)\n\
                  src/api.py:88:1: F401 'os' imported but unused\n\
                  Found 2 errors.";
    let compressed = compress_output("ruff check", output);
    let text = compressed.unwrap_or_else(|| output.to_string());
    assert!(
        text.contains("src/api.py:42:10"),
        "ruff must preserve file:line:col references: {text}"
    );
    assert!(
        text.contains("src/api.py:88:1"),
        "ruff must preserve all references: {text}"
    );
}

#[test]
fn regression_find_preserves_full_paths() {
    let output = "/home/user/project/src/api/file.ts\n\
                  /home/user/project/src/utils/helper.ts\n\
                  /home/user/project/tests/test_api.ts";
    let compressed = compress_output("find . -name '*.ts'", output);
    let text = compressed.unwrap_or_else(|| output.to_string());
    assert!(
        text.contains("/home/user/project/src/api/file.ts"),
        "find must preserve full absolute paths: {text}"
    );
}

#[test]
fn regression_ls_recursive_preserves_tree() {
    let output = "./src:\napi.ts\nutils.ts\n\n./src/components:\nButton.tsx\nHeader.tsx\n\n./tests:\ntest_api.ts";
    let compressed = compress_output("ls -R", output);
    let text = compressed.unwrap_or_else(|| output.to_string());
    assert!(
        text.contains("./src:") && text.contains("./tests:"),
        "ls -R must preserve directory headers: {text}"
    );
}

#[test]
fn regression_wc_pipe_correct() {
    let output = "42";
    let compressed = compress_output("wc -l", output);
    let text = compressed.unwrap_or_else(|| output.to_string());
    assert!(text.contains("42"), "wc output must be preserved: {text}");
}

// ===== New adversarial tests: comprehensive coverage across ecosystems =====

#[test]
fn adversarial_npm_install_preserves_packages() {
    let output = "\
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated inflight@1.0.6: This module is not supported

added 547 packages, and audited 548 packages in 12s

75 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities";

    let compressed = compress_output("npm install", output).unwrap();
    assert!(
        compressed.contains("547"),
        "npm install must preserve package count: {compressed}"
    );
}

#[test]
fn adversarial_npm_install_with_explicit_packages() {
    let output = "\
+ express@4.18.2
+ lodash@4.17.21
+ axios@1.6.2

added 58 packages, and audited 59 packages in 3s

found 0 vulnerabilities";

    let compressed = compress_output("npm install express lodash axios", output).unwrap();
    assert!(
        compressed.contains("express"),
        "npm install must preserve installed package names: {compressed}"
    );
    assert!(
        compressed.contains("lodash"),
        "npm install must preserve installed package names: {compressed}"
    );
    assert!(
        compressed.contains("axios"),
        "npm install must preserve installed package names: {compressed}"
    );
}

#[test]
fn adversarial_cargo_build_preserves_errors() {
    let output = "\
   Compiling myapp v0.1.0 (/home/user/myapp)
error[E0308]: mismatched types
 --> src/main.rs:42:10
  |
42|     foo(x)
  |         ^ expected `&str`, found `String`
  |

error[E0599]: no method named `bar` found for struct `Config`
  --> src/config.rs:15:10
   |
15 |     cfg.bar()
   |         ^^^ method not found in `Config`

error: could not compile `myapp` (bin \"myapp\") due to 2 previous errors";

    let compressed = compress_output("cargo build", output).unwrap();
    assert!(
        compressed.contains("E0308"),
        "cargo build must preserve error codes: {compressed}"
    );
    assert!(
        compressed.contains("E0599"),
        "cargo build must preserve all error codes: {compressed}"
    );
    assert!(
        compressed.contains("mismatched types") || compressed.contains("expected"),
        "cargo build must preserve error messages: {compressed}"
    );
}

#[test]
fn adversarial_eslint_preserves_file_line_and_rules() {
    let output = "\
/home/user/project/src/App.tsx
   5:10  error    'useState' is defined but never used  no-unused-vars
  12:15  warning  Unexpected any                        @typescript-eslint/no-explicit-any
  33:1   error    Missing return type                   @typescript-eslint/explicit-function-return-type

/home/user/project/src/utils.ts
   8:5   error    Unexpected var                        no-var

4 problems (3 errors, 1 warning)";

    let compressed = compress_output("eslint .", output).unwrap();
    assert!(
        compressed.contains("no-unused-vars") || compressed.contains("unused"),
        "eslint must preserve rule names: {compressed}"
    );
    assert!(
        compressed.contains("3 error") || compressed.contains("error"),
        "eslint must preserve error counts: {compressed}"
    );
}

#[test]
fn adversarial_go_build_preserves_errors() {
    let output = "\
./main.go:15:2: undefined: Config
./main.go:23:10: cannot use x (variable of type string) as int value in argument to process
./handlers/auth.go:42:5: too many arguments in call to validateToken
./handlers/auth.go:55:12: impossible type assertion: *http.Request does not implement CustomRequest";

    let result = compress_output("go build ./...", output).unwrap_or_else(|| output.to_string());
    assert!(
        result.contains("main.go") || result.contains("undefined"),
        "go build must preserve file references: {result}"
    );
    assert!(
        result.contains("auth.go") || result.contains("validateToken"),
        "go build must preserve all error locations: {result}"
    );
}

#[test]
fn adversarial_docker_build_preserves_step_errors() {
    let output = "\
#1 [internal] load build definition from Dockerfile
#1 DONE 0.0s

#5 [2/5] RUN apt-get update && apt-get install -y curl
#5 DONE 15.2s

#6 [3/5] COPY requirements.txt .
#6 DONE 0.1s

#7 [4/5] RUN pip install -r requirements.txt
#7 4.521 ERROR: Could not find a version that satisfies the requirement nonexistent-package==99.0
#7 4.521 ERROR: No matching distribution found for nonexistent-package==99.0
#7 ERROR: process \"/bin/sh -c pip install -r requirements.txt\" did not complete successfully: exit code: 1";

    let compressed = compress_output("docker build .", output).unwrap();
    assert!(
        compressed.contains("ERROR") || compressed.contains("error"),
        "docker build must preserve error lines: {compressed}"
    );
    assert!(
        compressed.contains("nonexistent-package") || compressed.contains("not complete"),
        "docker build must preserve error details: {compressed}"
    );
}

#[test]
fn adversarial_tsc_preserves_type_errors() {
    let output = "\
src/api/routes.ts(15,10): error TS2304: Cannot find name 'Request'.
src/api/routes.ts(22,5): error TS2339: Property 'userId' does not exist on type 'Session'.
src/utils/auth.ts(8,3): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
src/utils/auth.ts(42,20): error TS7006: Parameter 'req' implicitly has an 'any' type.

Found 4 errors in 2 files.";

    let compressed = compress_output("tsc --noEmit", output).unwrap();
    assert!(
        compressed.contains("TS2304"),
        "tsc must preserve error code TS2304: {compressed}"
    );
    assert!(
        compressed.contains("TS2339"),
        "tsc must preserve error code TS2339: {compressed}"
    );
    assert!(
        compressed.contains("routes.ts") || compressed.contains("auth.ts"),
        "tsc must preserve file references: {compressed}"
    );
    assert!(
        compressed.contains("4 error"),
        "tsc must preserve error count: {compressed}"
    );
}

#[test]
fn adversarial_dotnet_build_preserves_errors() {
    let output = "\
Microsoft (R) Build Engine version 17.8.3+195e7f5a3 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  All projects are up-to-date for restore.
Controllers/UserController.cs(15,25): error CS0246: The type or namespace name 'UserService' could not be found
Models/User.cs(8,12): error CS0246: The type or namespace name 'JsonProperty' could not be found

Build FAILED.

Controllers/UserController.cs(15,25): error CS0246: The type or namespace name 'UserService' could not be found
Models/User.cs(8,12): error CS0246: The type or namespace name 'JsonProperty' could not be found
    2 Error(s)
    0 Warning(s)

Time Elapsed 00:00:01.82";

    let compressed = compress_output("dotnet build", output).unwrap();
    assert!(
        compressed.contains("CS0246") || compressed.contains("error"),
        "dotnet build must preserve error codes: {compressed}"
    );
    assert!(
        compressed.contains("FAILED") || compressed.contains('2'),
        "dotnet build must preserve build result: {compressed}"
    );
}

#[test]
fn adversarial_composer_install_preserves_packages() {
    let output = "\
Loading composer repositories with package information
Updating dependencies
Lock file operations: 5 installs, 0 updates, 0 removals
  - Installing psr/log (3.0.0): Extracting archive
  - Installing monolog/monolog (3.5.0): Extracting archive
  - Installing symfony/console (7.0.3): Extracting archive
  - Installing laravel/framework (11.0.0): Extracting archive
  - Installing phpunit/phpunit (10.5.0): Extracting archive
Writing lock file
Generating optimized autoload files
Package operations: 5 installs, 0 updates, 0 removals";

    let compressed = compress_output("composer install", output).unwrap();
    assert!(
        compressed.contains('5') || compressed.contains("install"),
        "composer install must preserve package count: {compressed}"
    );
}

#[test]
fn adversarial_cargo_test_preserves_failures() {
    let output = "\
   Compiling myapp v0.1.0 (/home/user/myapp)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 2.34s
     Running unittests src/lib.rs (target/debug/deps/myapp-abc123)

running 25 tests
test auth::tests::login_works ... ok
test auth::tests::logout_works ... ok
test auth::tests::token_expired ... FAILED
test db::tests::connection_pool ... ok

failures:

---- auth::tests::token_expired stdout ----
thread 'auth::tests::token_expired' panicked at 'assertion failed: `(left == right)`
  left: `true`,
 right: `false`', src/auth.rs:142:9

failures:
    auth::tests::token_expired

test result: FAILED. 24 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.52s";

    let compressed = compress_output("cargo test", output).unwrap();
    assert!(
        compressed.contains("1 failed") || compressed.contains("FAILED"),
        "cargo test must preserve failure count: {compressed}"
    );
    assert!(
        compressed.contains("24 passed") || compressed.contains("24"),
        "cargo test must preserve passed count: {compressed}"
    );
}

#[test]
fn adversarial_kubectl_get_preserves_pod_status() {
    let output = "\
NAME                          READY   STATUS             RESTARTS      AGE
api-deploy-abc123-xyz         1/1     Running            0             5d
web-deploy-def456-uvw         0/1     CrashLoopBackOff   15 (2m ago)   1h
worker-deploy-ghi789-rst      1/1     Running            0             3d
db-migrate-job-abc            0/1     Error              0             30m";

    // kubectl get pods is Compressible (not Verbatim), but the kubectl
    // pattern engine must preserve all critical status information.
    match compress_output("kubectl get pods", output) {
        None => {} // Verbatim passthrough is also fine
        Some(compressed) => {
            assert!(
                compressed.contains("CrashLoopBackOff"),
                "must preserve CrashLoopBackOff status: {compressed}"
            );
            assert!(
                compressed.contains("Error"),
                "must preserve Error status: {compressed}"
            );
            assert!(
                compressed.contains("web-deploy") || compressed.contains("def456"),
                "must preserve failing pod identifier: {compressed}"
            );
        }
    }
}

#[test]
fn adversarial_terraform_plan_preserves_changes() {
    let output = "\
Terraform will perform the following actions:

  # aws_instance.web will be destroyed
  - resource \"aws_instance\" \"web\" {
      - ami           = \"ami-abc123\" -> null
      - instance_type = \"t3.large\" -> null
    }

  # aws_security_group.allow_all will be created
  + resource \"aws_security_group\" \"allow_all\" {
      + name        = \"allow_all\"
      + ingress {
          + from_port   = 0
          + to_port     = 65535
          + protocol    = \"-1\"
          + cidr_blocks = [\"0.0.0.0/0\"]
        }
    }

Plan: 1 to add, 0 to change, 1 to destroy.";

    let compressed = compress_output("terraform plan", output).unwrap();
    assert!(
        compressed.contains("destroy") || compressed.contains("1 to destroy"),
        "terraform plan must preserve destructive actions: {compressed}"
    );
    assert!(
        compressed.contains("add") || compressed.contains("1 to add"),
        "terraform plan must preserve additions: {compressed}"
    );
}

// ===== Issue #149: TheDecipherist security review — additional adversarial tests =====

#[test]
fn adversarial_git_show_preserves_diff_content() {
    let output = "\
commit abc1234def5678901234567890abcdef12345678
Author: Dev <dev@example.com>
Date:   Mon Jan 1 10:00:00 2024 +0000

    fix: remove fee from charge calculation

diff --git a/src/billing.rs b/src/billing.rs
--- a/src/billing.rs
+++ b/src/billing.rs
@@ -10,3 +10,3 @@
-    return charge(amount + fee);
+    return charge(amount);  // BUG: fee not applied
     log_transaction(amount);";

    let compressed = compress_output("git show abc1234", output).unwrap();
    assert!(
        compressed.contains("charge(amount)") || compressed.contains("fee not applied"),
        "git show must preserve diff code content: {compressed}"
    );
    assert!(
        compressed.contains('+') || compressed.contains('-'),
        "git show must preserve diff +/- markers: {compressed}"
    );
}

#[test]
fn adversarial_git_show_preserves_security_change() {
    let output = "\
commit deadbeef12345678901234567890abcdef12345678
Author: Dev <dev@example.com>
Date:   Mon Jan 1 10:00:00 2024 +0000

    chore: disable auth temporarily

diff --git a/src/auth.rs b/src/auth.rs
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -5,3 +5,5 @@
-    verify_csrf_token(&request);
+    // HACK: skip CSRF for now
+    // verify_csrf_token(&request);
     process_request(&request);";

    let compressed = compress_output("git show deadbeef", output).unwrap();
    assert!(
        compressed.contains("CSRF") || compressed.contains("csrf"),
        "git show must preserve security-relevant changes: {compressed}"
    );
    assert!(
        compressed.contains("verify_csrf_token"),
        "git show must preserve removed function calls: {compressed}"
    );
}

#[test]
fn adversarial_docker_ps_unhealthy_narrow_columns() {
    let output = "\
CONTAINER ID   IMAGE          COMMAND    CREATED      STATUS                    PORTS     NAMES
abc123def456   nginx:latest   \"nginx\"    2 hours ago  Up 2 hours (unhealthy)    80/tcp    web
789ghi012jkl   redis:7        \"redis\"    3 hours ago  Up 3 hours                6379/tcp  cache";

    // docker ps is Verbatim — output preserved unmodified.
    assert!(
        compress_output("docker ps", output).is_none(),
        "docker ps must be Verbatim (no compression)"
    );
}

#[test]
fn adversarial_docker_ps_exited_containers() {
    let output = "\
CONTAINER ID   IMAGE          COMMAND    CREATED      STATUS                        PORTS     NAMES
abc123def456   nginx:latest   \"nginx\"    2 hours ago  Exited (1) 30 minutes ago               web-crashed
789ghi012jkl   redis:7        \"redis\"    3 hours ago  Up 3 hours (healthy)          6379/tcp  cache";

    // docker ps -a is Verbatim — output preserved unmodified.
    assert!(
        compress_output("docker ps -a", output).is_none(),
        "docker ps -a must be Verbatim (no compression)"
    );
}

#[test]
fn adversarial_git_log_100_plus_commits() {
    let mut log_lines: Vec<String> = Vec::new();
    for i in 0..120 {
        log_lines.push(format!("abc{i:04x} fix: commit message number {i}"));
    }
    let output = log_lines.join("\n");

    let compressed = compress_output("git log --oneline", &output).unwrap();
    assert!(
        compressed.contains("commit message number 99"),
        "git log default should show at least 100 entries: {compressed}"
    );
    assert!(
        compressed.contains("20 more commits"),
        "git log should indicate truncated count: {compressed}"
    );
}

#[test]
fn adversarial_git_log_explicit_limit_unlimited() {
    let mut log_lines: Vec<String> = Vec::new();
    for i in 0..120 {
        log_lines.push(format!("abc{i:04x} fix: commit message number {i}"));
    }
    let output = log_lines.join("\n");

    let compressed = compress_output("git log -n 120 --oneline", &output).unwrap();
    assert!(
        compressed.contains("commit message number 119"),
        "git log with explicit -n must preserve all entries: {compressed}"
    );
    assert!(
        !compressed.contains("more commits"),
        "git log with explicit -n must not truncate: {compressed}"
    );
}

#[test]
fn adversarial_safeguard_ratio_prevents_over_compression() {
    use lean_ctx::core::compressor::safeguard_ratio;

    let original = "a]b ".repeat(200);
    let over_compressed = "x";
    let result = safeguard_ratio(&original, over_compressed);
    assert_eq!(
        result, original,
        "safeguard_ratio must return original when ratio < 0.05 and output is small"
    );

    let mild_compressed = "a]b ".repeat(80);
    let result2 = safeguard_ratio(&original, &mild_compressed);
    assert_eq!(
        result2, mild_compressed,
        "safeguard_ratio must allow mild compression"
    );
}

#[test]
fn adversarial_shell_hook_preserves_errors_in_truncation() {
    let mut lines: Vec<String> = Vec::new();
    for i in 0..100 {
        lines.push(format!("normal output line {i}"));
    }
    lines[50] = "CRITICAL: database corruption detected in row 4821".to_string();
    lines[75] = "ERROR: payment processing service unreachable".to_string();
    let output = lines.join("\n");

    let compressed = lean_ctx::shell::compress_if_beneficial_pub("cat /var/log/app.log", &output);
    assert!(
        compressed.contains("CRITICAL") || compressed.contains("database corruption"),
        "shell hook must preserve CRITICAL lines during truncation: {compressed}"
    );
    assert!(
        compressed.contains("ERROR") || compressed.contains("payment processing"),
        "shell hook must preserve ERROR lines during truncation: {compressed}"
    );
}

// --- gh CLI security regression tests (issue #149 follow-up) ---

#[test]
fn adversarial_gh_pr_diff_passes_through_verbatim() {
    let diff = r#"diff --git a/src/auth.rs b/src/auth.rs
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -42,7 +42,6 @@ fn validate_csrf_token(req: &Request) -> bool {
     let token = req.header("X-CSRF-Token");
-    if !csrf::verify(token, &session.secret) {
-        return false;
-    }
+    // SECURITY: CSRF check removed for performance
     true
 }
"#;
    let result = compress_output("gh pr diff 185", diff).unwrap_or_else(|| diff.to_string());
    assert!(
        result.contains("csrf::verify"),
        "gh pr diff must preserve all diff content: {result}"
    );
    assert!(
        result.contains("SECURITY: CSRF check removed"),
        "gh pr diff must preserve security-critical additions: {result}"
    );
}

#[test]
fn adversarial_gh_api_passes_through_verbatim() {
    let api_output = r#"[{"filename":"src/auth.rs","patch":"@@ -42,7 +42,6 @@\n-    csrf::verify(token)\n+    // removed"}]"#;
    let result = compress_output("gh api repos/org/repo/pulls/42/files", api_output)
        .unwrap_or_else(|| api_output.to_string());
    assert!(
        result.len() <= api_output.len(),
        "gh api output must not inflate: {} vs {}",
        result.len(),
        api_output.len()
    );
}

#[test]
fn adversarial_gh_search_passes_through_verbatim() {
    let search_output = (0..50)
        .map(|i| format!("repo/file{i}.rs:42: fn vulnerable_handler() {{"))
        .collect::<Vec<_>>()
        .join("\n");
    let result = compress_output("gh search code 'sql injection'", &search_output)
        .unwrap_or_else(|| search_output.clone());
    assert!(
        result.contains("vulnerable_handler"),
        "gh search output must preserve security-critical content: {result}"
    );
}

#[test]
fn adversarial_gh_workflow_passes_through_verbatim() {
    let workflow_output = "ID       STATUS     CONCLUSION  NAME\n123456   completed  failure     CI\n123457   completed  success     Deploy\n123458   in_progress            Security Scan";
    let result = compress_output("gh workflow view ci.yml", workflow_output)
        .unwrap_or_else(|| workflow_output.to_string());
    assert!(
        result.contains("failure") && result.contains("CI"),
        "gh workflow output must preserve status info: {result}"
    );
}

#[test]
fn adversarial_gh_pr_list_still_compresses() {
    let pr_list = "#185\tSupport configurable proxy\tzsefvlol:feat/proxy\tDRAFT\n#184\tFix auth bypass\tsecurity-team:fix/auth\tOPEN\n";
    let result = compress_output("gh pr list", pr_list).unwrap_or_else(|| pr_list.to_string());
    assert!(
        result.contains("auth bypass") || result.contains("Fix auth"),
        "gh pr list output must preserve PR info: {result}"
    );
}

#[test]
fn adversarial_gh_diff_preserves_security_change() {
    let diff = format!(
        "diff --git a/middleware/auth.js b/middleware/auth.js\n{}\n{}",
        "- const isAdmin = await checkAdminRole(user);",
        "+ const isAdmin = true; // HACK: bypassed for demo"
    );
    let shell_result = lean_ctx::shell::compress_if_beneficial_pub("gh pr diff 200", &diff);
    assert!(
        shell_result.contains("isAdmin = true") || shell_result.contains("HACK"),
        "gh pr diff through shell hook must preserve security-critical diff content: {shell_result}"
    );
}

#[test]
fn rg_large_output_compresses_through_pipeline() {
    let mut lines = Vec::new();
    for i in 0..500 {
        for f in &[
            "src/core/mod.rs",
            "src/cli/dispatch.rs",
            "src/tools/ctx_shell.rs",
            "src/server/mod.rs",
            "src/lib.rs",
        ] {
            lines.push(format!("{f}:{}: fn func_{i}() {{", i + 1));
        }
    }
    let output = lines.join("\n");
    let input_len = output.len();

    let result =
        lean_ctx::shell::compress_if_beneficial_pub("rg 'fn ' --no-heading -n src/", &output);

    assert!(
        result.len() < input_len,
        "rg pipeline must compress: result={} vs original={}",
        result.len(),
        input_len
    );
    assert!(
        !result.contains("[lean-ctx:"),
        "savings footer must be silent by default"
    );
}
