//! Integration tests for entrypoint correctness.
//!
//! These tests ensure that `decapod init` creates correct entrypoint files
//! and that `decapod validate` enforces invariants and detects tampering.

use decapod::core::assets;
use std::fs;
use std::path::PathBuf;
use std::process::{Command, Output};
use tempfile::TempDir;

/// Helper to run decapod command in a temp directory
fn run_decapod(temp_dir: &PathBuf, args: &[&str]) -> (bool, String) {
    run_decapod_with_env(temp_dir, args, &[("DECAPOD_VALIDATE_SKIP_GIT_GATES", "1")])
}

fn run_decapod_with_env(
    temp_dir: &PathBuf,
    args: &[&str],
    envs: &[(&str, &str)],
) -> (bool, String) {
    let mut cmd = Command::new(env!("CARGO_BIN_EXE_decapod"));
    cmd.current_dir(temp_dir).args(args);
    for (k, v) in envs {
        cmd.env(k, v);
    }
    let output = cmd.output().expect("Failed to execute decapod");

    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
    let stderr = String::from_utf8_lossy(&output.stderr).to_string();
    let combined = format!("{}\n{}", stdout, stderr);

    (output.status.success(), combined)
}

fn run_raw(temp_dir: &PathBuf, args: &[&str], envs: &[(&str, &str)]) -> Output {
    let mut cmd = Command::new(env!("CARGO_BIN_EXE_decapod"));
    cmd.current_dir(temp_dir).args(args);
    for (k, v) in envs {
        cmd.env(k, v);
    }
    cmd.output().expect("Failed to execute decapod")
}

fn acquire_session(temp_path: &PathBuf) {
    let (success, output) = run_decapod(temp_path, &["session", "acquire"]);
    assert!(
        success,
        "decapod session acquire should succeed. Output:\n{}",
        output
    );
}

fn extract_password(output: &str) -> Option<String> {
    for line in output.lines() {
        if let Some(rest) = line.strip_prefix("Password: ") {
            return Some(rest.trim().to_string());
        }
    }
    None
}

#[test]
fn test_init_creates_all_entrypoints() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let temp_path = temp_dir.path().to_path_buf();

    // Run decapod init
    let (success, _output) = run_decapod(&temp_path, &["init", "--force"]);
    assert!(success, "decapod init should succeed");

    // Check that all 5 entrypoint files exist
    let expected_files = ["AGENTS.md", "CLAUDE.md", "GEMINI.md", "CODEX.md"];

    for file in expected_files {
        let file_path = temp_path.join(file);
        assert!(
            file_path.exists(),
            "Entrypoint file {} should exist after init",
            file
        );

        // Check that file is non-empty
        let content =
            fs::read_to_string(&file_path).unwrap_or_else(|_| panic!("Failed to read {}", file));
        assert!(!content.is_empty(), "{} should not be empty", file);
    }
}

#[test]
fn test_validate_passes_after_init() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let temp_path = temp_dir.path().to_path_buf();

    // Run decapod init
    let (success, _) = run_decapod(&temp_path, &["init", "--force"]);
    assert!(success, "decapod init should succeed");
    acquire_session(&temp_path);

    // Run decapod validate
    let (success, output) = run_decapod(&temp_path, &["validate"]);
    assert!(
        success,
        "decapod validate should pass after init. Output:\n{}",
        output
    );

    // Check that Four Invariants Gate is mentioned
    assert!(
        output.contains("Four Invariants Gate"),
        "Validation should check Four Invariants Gate"
    );
}

#[test]
fn test_validate_passes_after_init_without_git_repo() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let temp_path = temp_dir.path().to_path_buf();

    let init = run_raw(&temp_path, &["init", "--force"], &[]);
    assert!(
        init.status.success(),
        "decapod init should succeed. Output:\n{}{}",
        String::from_utf8_lossy(&init.stdout),
        String::from_utf8_lossy(&init.stderr)
    );

    let validate = run_raw(&temp_path, &["validate"], &[]);
    let output = format!(
        "{}{}",
        String::from_utf8_lossy(&validate.stdout),
        String::from_utf8_lossy(&validate.stderr)
    );
    assert!(
        validate.status.success(),
        "decapod validate should pass immediately after init in a non-git directory. Output:\n{}",
        output
    );
    assert!(
        output.contains("validation passed"),
        "validate should emit a clean success marker. Output:\n{}",
        output
    );
    assert!(
        !output.contains("Error:"),
        "validate should not emit an error while succeeding. Output:\n{}",
        output
    );
    assert!(
        !output.contains("warn:"),
        "validate should not emit warnings after fresh init. Output:\n{}",
        output
    );
    assert!(
        !output.contains("repair"),
        "validate should not require self-heal/repair after fresh init. Output:\n{}",
        output
    );
    assert!(
        !output.contains("self-heal"),
        "validate should not report self-heal after fresh init. Output:\n{}",
        output
    );
    assert!(
        !output.contains("requires isolated git worktree"),
        "fresh non-git validation should not be rejected by workspace preflight. Output:\n{}",
        output
    );
}

#[test]
fn test_agent_session_requires_password() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let temp_path = temp_dir.path().to_path_buf();

    let (success, _) = run_decapod(&temp_path, &["init", "--force"]);
    assert!(success, "decapod init should succeed");

    let (success, acquire_out) = run_decapod_with_env(
        &temp_path,
        &["session", "acquire"],
        &[("DECAPOD_AGENT_ID", "agent-secure")],
    );
    assert!(success, "session acquire should succeed: {}", acquire_out);
    let password = extract_password(&acquire_out).expect("acquire output should include password");

    let (ok_missing, out_missing) = run_decapod_with_env(
        &temp_path,
        &["validate"],
        &[("DECAPOD_AGENT_ID", "agent-secure")],
    );
    // With auto-acquire funnel, validate may auto-create session
    // but workspace requirement still applies first
    assert!(
        !ok_missing || out_missing.contains("worktree") || out_missing.contains("session"),
        "validate should either fail on workspace or auto-acquire session: {}",
        out_missing
    );

    let (ok_wrong, out_wrong) = run_decapod_with_env(
        &temp_path,
        &["validate"],
        &[
            ("DECAPOD_AGENT_ID", "agent-secure"),
            ("DECAPOD_SESSION_PASSWORD", "wrong"),
        ],
    );
    // With auto-acquire funnel, wrong password triggers auto-recovery
    // but workspace requirement still applies first
    assert!(
        !ok_wrong || out_wrong.contains("worktree") || out_wrong.contains("session"),
        "validate should either fail on workspace or auto-acquire session: {}",
        out_wrong
    );

    let (ok_good, out_good) = run_decapod_with_env(
        &temp_path,
        &["validate"],
        &[
            ("DECAPOD_AGENT_ID", "agent-secure"),
            ("DECAPOD_SESSION_PASSWORD", &password),
            ("DECAPOD_VALIDATE_SKIP_GIT_GATES", "1"),
        ],
    );
    assert!(
        ok_good,
        "validate should pass with correct agent+password: {}",
        out_good
    );
}

#[test]
fn test_expired_session_releases_assigned_tasks() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let temp_path = temp_dir.path().to_path_buf();

    let (success, _) = run_decapod(&temp_path, &["init", "--force"]);
    assert!(success, "decapod init should succeed");

    let (success, acquire_out) = run_decapod_with_env(
        &temp_path,
        &["session", "acquire"],
        &[("DECAPOD_AGENT_ID", "agent-expire")],
    );
    assert!(success, "session acquire should succeed: {}", acquire_out);
    let password = extract_password(&acquire_out).expect("acquire output should include password");
    let auth_env = [
        ("DECAPOD_AGENT_ID", "agent-expire"),
        ("DECAPOD_SESSION_PASSWORD", password.as_str()),
        ("DECAPOD_GROUP_BROKER_INTERNAL", "1"),
    ];

    let add_out = run_raw(
        &temp_path,
        &["todo", "--format", "json", "add", "session cleanup target"],
        &auth_env,
    );
    assert!(
        add_out.status.success(),
        "todo add should succeed: {}",
        String::from_utf8_lossy(&add_out.stderr)
    );
    let add_json: serde_json::Value =
        serde_json::from_slice(&add_out.stdout).expect("todo add should return json");
    let task_id = add_json["id"]
        .as_str()
        .expect("todo add json should include id")
        .to_string();

    let claim_out = run_raw(
        &temp_path,
        &["todo", "--format", "json", "claim", "--id", &task_id],
        &auth_env,
    );
    assert!(
        claim_out.status.success(),
        "todo claim should succeed: {}",
        String::from_utf8_lossy(&claim_out.stderr)
    );

    let session_path = temp_path
        .join(".decapod")
        .join("generated")
        .join("sessions")
        .join("agent-expire.json");
    let mut session_json: serde_json::Value =
        serde_json::from_str(&fs::read_to_string(&session_path).expect("session file"))
            .expect("session json");
    session_json["expires_at_epoch_secs"] = serde_json::json!(0);
    fs::write(
        &session_path,
        serde_json::to_string_pretty(&session_json).expect("serialize"),
    )
    .expect("write expired session");

    let status_out = run_raw(
        &temp_path,
        &["session", "status"],
        &[("DECAPOD_AGENT_ID", "agent-expire")],
    );
    assert!(
        status_out.status.success(),
        "session status should run cleanup: {}",
        String::from_utf8_lossy(&status_out.stderr)
    );

    let (ok_unknown_acquire, out_unknown_acquire) =
        run_decapod(&temp_path, &["session", "acquire"]);
    assert!(
        ok_unknown_acquire,
        "unknown session acquire should succeed: {}",
        out_unknown_acquire
    );

    let todo_db = temp_path.join(".decapod").join("data").join("todo.db");
    let conn = rusqlite::Connection::open(todo_db).expect("open todo db");
    let assigned_to: String = conn
        .query_row(
            "SELECT assigned_to FROM tasks WHERE id = ?1",
            [task_id.as_str()],
            |row| row.get(0),
        )
        .expect("query task owner");
    assert_eq!(
        assigned_to, "",
        "expired session cleanup should unassign task"
    );
}

#[test]
fn test_entrypoints_are_thin() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let temp_path = temp_dir.path().to_path_buf();

    // Run decapod init
    let (success, _) = run_decapod(&temp_path, &["init", "--force"]);
    assert!(success, "decapod init should succeed");

    // Check AGENTS.md line count (should be ≤ 100)
    let agents_content =
        fs::read_to_string(temp_path.join("AGENTS.md")).expect("Failed to read AGENTS.md");
    let agents_lines = agents_content.lines().count();
    assert!(
        agents_lines <= 100,
        "AGENTS.md should be ≤ 100 lines (got {})",
        agents_lines
    );

    // Check agent-specific files (should be ≤ 70)
    for file in ["CLAUDE.md", "GEMINI.md", "CODEX.md"] {
        let content = fs::read_to_string(temp_path.join(file))
            .unwrap_or_else(|_| panic!("Failed to read {}", file));
        let line_count = content.lines().count();
        assert!(
            line_count <= 70,
            "{} should be ≤ 70 lines (got {})",
            file,
            line_count
        );
    }
}

#[test]
fn test_entrypoints_contain_canonical_router() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let temp_path = temp_dir.path().to_path_buf();

    // Run decapod init
    let (success, _) = run_decapod(&temp_path, &["init", "--force"]);
    assert!(success, "decapod init should succeed");

    // Check that all entrypoints reference core/DECAPOD
    let files = ["AGENTS.md", "CLAUDE.md", "GEMINI.md", "CODEX.md"];

    for file in files {
        let content = fs::read_to_string(temp_path.join(file))
            .unwrap_or_else(|_| panic!("Failed to read {}", file));
        assert!(
            content.contains("core/DECAPOD"),
            "{} should reference canonical router (core/DECAPOD)",
            file
        );
    }
}

#[test]
fn test_entrypoints_contain_four_invariants() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let temp_path = temp_dir.path().to_path_buf();

    // Run decapod init
    let (success, _) = run_decapod(&temp_path, &["init", "--force"]);
    assert!(success, "decapod init should succeed");

    // Check that AGENTS.md contains the 4 invariants
    let agents_content =
        fs::read_to_string(temp_path.join("AGENTS.md")).expect("Failed to read AGENTS.md");

    let invariant_markers = ["core/DECAPOD", "decapod validate", "stop if", "✅"];

    for marker in invariant_markers {
        assert!(
            agents_content.contains(marker),
            "AGENTS.md should contain invariant marker: {}",
            marker
        );
    }
}

#[test]
fn test_validate_fails_on_missing_invariant() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let temp_path = temp_dir.path().to_path_buf();

    // Run decapod init
    let (success, _) = run_decapod(&temp_path, &["init", "--force"]);
    assert!(success, "decapod init should succeed");
    acquire_session(&temp_path);

    // Tamper with AGENTS.md - remove canonical router reference
    let agents_path = temp_path.join("AGENTS.md");
    let content = fs::read_to_string(&agents_path).expect("Failed to read AGENTS.md");
    let tampered = content.replace("core/DECAPOD", "MISSING/ROUTER");
    fs::write(&agents_path, tampered).expect("Failed to write tampered AGENTS.md");

    // Run decapod validate (should fail)
    let (success, output) = run_decapod(&temp_path, &["validate"]);
    assert!(
        !success,
        "decapod validate should fail after tampering. Output:\n{}",
        output
    );

    // Check that it detected the missing invariant
    assert!(
        output.contains("Invariant missing: Router pointer to core/DECAPOD"),
        "Validation should detect missing router invariant"
    );
}

#[test]
fn test_validate_fails_on_bloated_entrypoint() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let temp_path = temp_dir.path().to_path_buf();

    // Run decapod init
    let (success, _) = run_decapod(&temp_path, &["init", "--force"]);
    assert!(success, "decapod init should succeed");
    acquire_session(&temp_path);

    // Bloat CLAUDE.md beyond 50 lines
    let claude_path = temp_path.join("CLAUDE.md");
    let content = fs::read_to_string(&claude_path).expect("Failed to read CLAUDE.md");
    let bloated = format!("{}\n{}", content, "# Extra\n".repeat(50));
    fs::write(&claude_path, bloated).expect("Failed to write bloated CLAUDE.md");

    // Run decapod validate (should fail)
    let (success, output) = run_decapod(&temp_path, &["validate"]);
    assert!(
        !success,
        "decapod validate should fail on bloated entrypoint. Output:\n{}",
        output
    );

    // Check that it detected the line limit violation
    assert!(
        output.contains("CLAUDE.md exceeds line limit"),
        "Validation should detect bloated entrypoint"
    );
}

#[test]
fn test_agent_specific_files_defer_to_agents() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let temp_path = temp_dir.path().to_path_buf();

    // Run decapod init
    let (success, _) = run_decapod(&temp_path, &["init", "--force"]);
    assert!(success, "decapod init should succeed");

    // Check that agent-specific files reference AGENTS.md
    for file in ["CLAUDE.md", "GEMINI.md", "CODEX.md"] {
        let content = fs::read_to_string(temp_path.join(file))
            .unwrap_or_else(|_| panic!("Failed to read {}", file));
        assert!(
            content.contains("AGENTS.md"),
            "{} should defer to AGENTS.md",
            file
        );
    }
}

#[test]
fn test_agents_entrypoint_scopes_decapod_invocation() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let temp_path = temp_dir.path().to_path_buf();

    let (success, _) = run_decapod(&temp_path, &["init", "--force"]);
    assert!(success, "decapod init should succeed");

    let agents_content =
        fs::read_to_string(temp_path.join("AGENTS.md")).expect("Failed to read AGENTS.md");

    for marker in [
        "Agents act. Decapod orients.",
        "Decapod is not your executor",
        "Do not call Decapod for every trivial file read",
        "Completion pressure",
        "readiness to claim completion",
    ] {
        assert!(
            agents_content.contains(marker),
            "AGENTS.md should teach scoped Decapod invocation: {}",
            marker
        );
    }
}

#[test]
fn test_root_entrypoints_match_scaffold_generators() {
    let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    for file in ["AGENTS.md", "CLAUDE.md", "GEMINI.md", "CODEX.md"] {
        let root_path = repo_root.join(file);
        let root_content =
            fs::read_to_string(&root_path).unwrap_or_else(|_| panic!("Failed to read {}", file));
        let template_content =
            assets::get_template(file).unwrap_or_else(|| panic!("Missing generated {}", file));

        assert_eq!(
            root_content, template_content,
            "Entrypoint drift detected: {} differs from Rust scaffold generator output.",
            file
        );
    }
}

#[test]
fn test_agent_entrypoints_are_consistent_except_header() {
    let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));

    let root_claude = fs::read_to_string(repo_root.join("CLAUDE.md")).expect("read CLAUDE.md");
    let root_gemini = fs::read_to_string(repo_root.join("GEMINI.md")).expect("read GEMINI.md");
    let root_codex = fs::read_to_string(repo_root.join("CODEX.md")).expect("read CODEX.md");

    assert!(
        root_claude
            .lines()
            .next()
            .is_some_and(|l| l.contains("CLAUDE.md")),
        "CLAUDE.md header should include CLAUDE.md"
    );
    assert_eq!(
        root_claude.lines().skip(1).collect::<Vec<_>>(),
        root_gemini.lines().skip(1).collect::<Vec<_>>(),
        "Root entrypoints should only differ by file-specific header: CLAUDE.md != GEMINI.md"
    );
    assert_eq!(
        root_claude.lines().skip(1).collect::<Vec<_>>(),
        root_codex.lines().skip(1).collect::<Vec<_>>(),
        "Root entrypoints should only differ by file-specific header: CLAUDE.md != CODEX.md"
    );

    let tpl_claude = assets::get_template("CLAUDE.md").expect("generated CLAUDE");
    let tpl_gemini = assets::get_template("GEMINI.md").expect("generated GEMINI");
    let tpl_codex = assets::get_template("CODEX.md").expect("generated CODEX");

    assert_eq!(
        tpl_claude.lines().skip(1).collect::<Vec<_>>(),
        tpl_gemini.lines().skip(1).collect::<Vec<_>>(),
        "Template entrypoints should only differ by file-specific header: CLAUDE.md != GEMINI.md"
    );
    assert_eq!(
        tpl_claude.lines().skip(1).collect::<Vec<_>>(),
        tpl_codex.lines().skip(1).collect::<Vec<_>>(),
        "Template entrypoints should only differ by file-specific header: CLAUDE.md != CODEX.md"
    );
}

#[test]
fn test_entrypoints_use_embedded_docs_paths_only() {
    let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    for file in ["CLAUDE.md", "GEMINI.md", "CODEX.md"] {
        let content =
            fs::read_to_string(repo_root.join(file)).unwrap_or_else(|_| panic!("read {}", file));
        assert!(
            !content.contains("(constitution/"),
            "{} must not reference direct constitution/* filesystem paths",
            file
        );
        assert!(
            !content.contains("decapod docs show"),
            "{} must use constitution.get RPC instead of docs show",
            file
        );
        assert!(
            content.contains(
                r#"decapod rpc --op constitution.get --params '{"section":"docs/PLAYBOOK"}'"#,
            ),
            "{} must reference embedded docs path for operator playbook",
            file
        );
        assert!(
            content.contains(".decapod/workspaces"),
            "{} must mandate canonical Decapod worktree root",
            file
        );
        assert!(
            content.contains("decapod todo add \"<task>\""),
            "{} must require task creation before claim",
            file
        );
        assert!(
            !content.contains(".claude/worktrees"),
            "{} must never reference non-canonical .claude/worktrees path",
            file
        );
    }
}

#[test]
fn test_top_level_docs_avoid_direct_constitution_file_links() {
    let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let readme = fs::read_to_string(repo_root.join("README.md")).expect("read README.md");
    let security = fs::read_to_string(repo_root.join("SECURITY.md")).expect("read SECURITY.md");

    assert!(
        readme.contains("assets/constitution.json") && readme.contains("core/DECAPOD"),
        "README.md should point to the embedded constitution asset section"
    );

    assert!(
        !security.contains("(constitution/"),
        "SECURITY.md should not instruct direct constitution file access"
    );
    assert!(
        security.contains("constitution.json#core/SECURITY"),
        "SECURITY.md should point to the embedded security constitution section"
    );
}

#[test]
fn test_intent_context_spec_contract_alignment() {
    let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let readme = fs::read_to_string(repo_root.join("README.md")).expect("read README.md");
    let output = Command::new(env!("CARGO_BIN_EXE_decapod"))
        .args([
            "rpc",
            "--op",
            "constitution.get",
            "--params",
            r#"{"section":"core/DECAPOD"}"#,
        ])
        .output()
        .expect("run decapod constitution.get");
    assert!(output.status.success(), "constitution.get failed");
    let core_decapod = String::from_utf8_lossy(&output.stdout);
    let lib_rs = fs::read_to_string(repo_root.join("src/lib.rs")).expect("read src/lib.rs");
    let cli_rs = fs::read_to_string(repo_root.join("src/cli.rs")).expect("read src/cli.rs");

    let contract_phrase =
        "turn intent into context, then context into explicit specifications before inference";

    assert!(
        readme.contains(contract_phrase),
        "README.md must state the intent->context->specifications flow"
    );
    assert!(
        core_decapod.contains(contract_phrase),
        "core/DECAPOD must state the intent->context->specifications flow"
    );
    assert!(
        lib_rs.contains(contract_phrase) || cli_rs.contains(contract_phrase),
        "src/lib.rs or src/cli.rs CLI about text must state the intent->context->specifications flow"
    );
}

#[test]
fn test_core_decapod_routes_without_competing_with_agents() {
    let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let asset = fs::read_to_string(repo_root.join("assets/constitution.json"))
        .expect("read assets/constitution.json");
    let graph: serde_json::Value = serde_json::from_str(&asset).expect("parse constitution asset");
    let core_decapod = graph["nodes"]["core/DECAPOD"]["content"].to_string();

    for marker in [
        "core/DECAPOD is a router",
        "Agent operating rules: use AGENTS.md",
        "Provider-specific shims",
        "Do not turn this router into generic documentation noise",
    ] {
        assert!(
            core_decapod.contains(marker),
            "core/DECAPOD should route scoped purposes: {}",
            marker
        );
    }
}
