mod common;

use std::fs;

use common::{
    copy_fixture_to_temp, fixture_path, normalize, run_command, run_short_command,
    run_short_command_in,
};

#[test]
fn check_command_succeeds_for_basic_fixture() {
    let manifest_path = fixture_path("basic");
    let output = run_command(&[
        "check",
        "--manifest-path",
        manifest_path
            .to_str()
            .expect("fixture path should be UTF-8"),
    ]);

    assert!(
        output.status.success(),
        "stderr:\n{}",
        normalize(&output.stderr)
    );
    assert_eq!(
        normalize(&output.stdout),
        "validated 8 feature(s) and 1 group(s): 0 error(s), 0 warning(s)\n"
    );
}

#[test]
fn short_binary_and_aliases_work() {
    let manifest_path = fixture_path("basic");
    let check_output = run_short_command(&[
        "fm",
        "c",
        "-m",
        manifest_path
            .to_str()
            .expect("fixture path should be UTF-8"),
    ]);

    assert!(
        check_output.status.success(),
        "stderr:\n{}",
        normalize(&check_output.stderr)
    );
    assert_eq!(
        normalize(&check_output.stdout),
        "validated 8 feature(s) and 1 group(s): 0 error(s), 0 warning(s)\n"
    );

    let lints_output = run_short_command(&["lints"]);
    assert!(lints_output.status.success());
    assert!(normalize(&lints_output.stdout).contains("missing-metadata"));

    let lint_docs_output = run_short_command(&["lints", "--markdown"]);
    assert!(lint_docs_output.status.success());
    assert!(normalize(&lint_docs_output.stdout).contains("# Lint Reference"));
}

#[test]
fn check_json_format_reports_issues() {
    let temp_dir = copy_fixture_to_temp("basic");
    let manifest_path = temp_dir.path().join("Cargo.toml");
    let manifest = fs::read_to_string(&manifest_path).expect("failed to read temp manifest");
    let updated_manifest = manifest.replace(
        "std = { description = \"Enables the standard library surface.\" }\n",
        "",
    );
    fs::write(&manifest_path, updated_manifest).expect("failed to write temp manifest");

    let output = run_command(&[
        "check",
        "--format",
        "json",
        "--manifest-path",
        manifest_path.to_str().expect("temp path should be UTF-8"),
    ]);

    assert!(
        !output.status.success(),
        "stdout:\n{}",
        normalize(&output.stdout)
    );
    let stdout = normalize(&output.stdout);
    assert!(stdout.contains("\"schema_version\": 1"));
    assert!(stdout.contains("\"code\": \"missing-metadata\""));
}

#[test]
fn check_formats_emit_github_and_sarif() {
    let temp_dir = copy_fixture_to_temp("basic");
    let manifest_path = temp_dir.path().join("Cargo.toml");
    let manifest = fs::read_to_string(&manifest_path).expect("failed to read temp manifest");
    let updated_manifest = manifest.replace(
        "docs-preview = { description = \"Generates docs | preview output.\\nIncludes async examples.\", category = \"docs\", since = \"0.2.0\", docs = \"https://docs.rs/feature-manifest\", tracking_issue = \"https://github.com/funwithcthulhu/feature-manifest/issues/1\", requires = [\"serde\"], note = \"Escapes table cells and Mermaid labels.\" }\n",
        "",
    );
    fs::write(&manifest_path, updated_manifest).expect("failed to write temp manifest");

    let github_output = run_command(&[
        "check",
        "--format",
        "github",
        "--manifest-path",
        manifest_path.to_str().expect("temp path should be UTF-8"),
    ]);
    assert!(!github_output.status.success());
    assert!(normalize(&github_output.stdout).contains(
        "::error file=Cargo.toml,line=14,col=1,title=feature-manifest missing-metadata::"
    ));

    let sarif_output = run_command(&[
        "check",
        "--format",
        "sarif",
        "--manifest-path",
        manifest_path.to_str().expect("temp path should be UTF-8"),
    ]);
    assert!(!sarif_output.status.success());
    assert!(normalize(&sarif_output.stdout).contains("\"version\": \"2.1.0\""));
    assert!(normalize(&sarif_output.stdout).contains("\"ruleId\": \"missing-metadata\""));
}

#[test]
fn check_lint_overrides_can_change_exit_behavior() {
    let temp_dir = copy_fixture_to_temp("basic");
    let manifest_path = temp_dir.path().join("Cargo.toml");
    let manifest = fs::read_to_string(&manifest_path).expect("failed to read temp manifest");
    let updated_manifest =
        manifest.replace("rustls = { description = \"Use rustls for TLS.\" }\n", "");
    fs::write(&manifest_path, updated_manifest).expect("failed to write temp manifest");

    let failing = run_command(&[
        "check",
        "--manifest-path",
        manifest_path.to_str().expect("temp path should be UTF-8"),
    ]);
    assert!(!failing.status.success());

    let passing = run_command(&[
        "check",
        "--lint",
        "missing-metadata=allow",
        "--lint",
        "missing-description=allow",
        "--manifest-path",
        manifest_path.to_str().expect("temp path should be UTF-8"),
    ]);
    assert!(
        passing.status.success(),
        "stderr:\n{}",
        normalize(&passing.stderr)
    );
}

#[test]
fn check_preset_can_soften_adoption_failures() {
    let temp_dir = copy_fixture_to_temp("basic");
    let manifest_path = temp_dir.path().join("Cargo.toml");
    let manifest = fs::read_to_string(&manifest_path).expect("failed to read temp manifest");
    let updated_manifest =
        manifest.replace("rustls = { description = \"Use rustls for TLS.\" }\n", "");
    fs::write(&manifest_path, updated_manifest).expect("failed to write temp manifest");

    let output = run_command(&[
        "check",
        "--preset",
        "adopt",
        "--manifest-path",
        manifest_path.to_str().expect("temp path should be UTF-8"),
    ]);

    assert!(
        output.status.success(),
        "stderr:\n{}",
        normalize(&output.stderr)
    );
    assert!(normalize(&output.stderr).contains("warning[missing-metadata]"));
}

#[test]
fn workspace_root_requires_explicit_package_selection() {
    let manifest_path = fixture_path("workspace");
    let output = run_command(&[
        "check",
        "--manifest-path",
        manifest_path
            .to_str()
            .expect("fixture path should be UTF-8"),
    ]);

    assert!(
        !output.status.success(),
        "stdout:\n{}",
        normalize(&output.stdout)
    );
    assert!(normalize(&output.stderr).contains("use `--workspace` or `--package <name>`"));
}

#[test]
fn workspace_check_reports_all_selected_packages() {
    let manifest_path = fixture_path("workspace");
    let output = run_command(&[
        "--workspace",
        "check",
        "--manifest-path",
        manifest_path
            .to_str()
            .expect("fixture path should be UTF-8"),
    ]);

    assert!(
        output.status.success(),
        "stderr:\n{}",
        normalize(&output.stderr)
    );
    let stdout = normalize(&output.stdout);
    assert!(stdout.contains("package `workspace-cli-fixture`"));
    assert!(stdout.contains("package `workspace-core-fixture`"));
    assert!(normalize(&output.stderr).contains("workspace summary: validated 2 package(s), 7 feature(s), 1 group(s): 0 error(s), 0 warning(s)"));
}

#[test]
fn explain_reports_feature_details() {
    let manifest_path = fixture_path("basic");
    let output = run_command(&[
        "explain",
        "docs-preview",
        "--manifest-path",
        manifest_path
            .to_str()
            .expect("fixture path should be UTF-8"),
    ]);

    assert!(
        output.status.success(),
        "stderr:\n{}",
        normalize(&output.stderr)
    );
    let stdout = normalize(&output.stdout);
    assert!(stdout.contains("Feature: `docs-preview`"));
    assert!(stdout.contains("Package: feature-manifest-fixture"));
    assert!(stdout.contains("Enables: `serde`, `tokio?/rt`"));
    assert!(stdout.contains("Included in default feature set: no"));
}

#[test]
fn sync_scaffolds_missing_metadata_entries() {
    let temp_dir = copy_fixture_to_temp("basic");
    let manifest_path = temp_dir.path().join("Cargo.toml");

    let manifest = fs::read_to_string(&manifest_path).expect("failed to read temp manifest");
    let updated_manifest = manifest.replace(
        "unstable = { description = \"Experimental APIs; semver not guaranteed.\", unstable = true }\n",
        "",
    );
    fs::write(&manifest_path, updated_manifest).expect("failed to write temp manifest");

    let output = run_command(&[
        "sync",
        "--manifest-path",
        manifest_path.to_str().expect("temp path should be UTF-8"),
    ]);

    assert!(
        output.status.success(),
        "stderr:\n{}",
        normalize(&output.stderr)
    );
    let stdout = normalize(&output.stdout);
    assert!(stdout.contains("synced `feature-manifest-fixture`"));
    assert!(stdout.contains("unstable"));

    let rewritten_manifest =
        fs::read_to_string(&manifest_path).expect("failed to read rewritten manifest");
    assert!(
        rewritten_manifest.contains("unstable = { description = \"TODO: describe `unstable`.\" }")
    );
}

#[test]
fn sync_check_remove_stale_and_style_flags_work() {
    let temp_dir = copy_fixture_to_temp("basic");
    let manifest_path = temp_dir.path().join("Cargo.toml");

    let manifest = fs::read_to_string(&manifest_path).expect("failed to read temp manifest");
    let stale_manifest = manifest
        .replace(
            "[package.metadata.feature-manifest.features]\n",
            "[package.metadata.feature-manifest]\nlegacy = { description = \"legacy\" }\n\n[package.metadata.feature-manifest.features]\n",
        )
        .replace(
            "docs-preview = { description = \"Generates docs | preview output.\\nIncludes async examples.\", category = \"docs\", since = \"0.2.0\", docs = \"https://docs.rs/feature-manifest\", tracking_issue = \"https://github.com/funwithcthulhu/feature-manifest/issues/1\", requires = [\"serde\"], note = \"Escapes table cells and Mermaid labels.\" }\n",
            "",
        );
    fs::write(&manifest_path, stale_manifest).expect("failed to write temp manifest");

    let check_output = run_command(&[
        "sync",
        "--check",
        "--remove-stale",
        "--style",
        "structured",
        "--manifest-path",
        manifest_path.to_str().expect("temp path should be UTF-8"),
    ]);
    assert!(!check_output.status.success());
    assert!(normalize(&check_output.stdout).contains("sync drift"));

    let apply_output = run_command(&[
        "sync",
        "--remove-stale",
        "--style",
        "structured",
        "--manifest-path",
        manifest_path.to_str().expect("temp path should be UTF-8"),
    ]);
    assert!(
        apply_output.status.success(),
        "stderr:\n{}",
        normalize(&apply_output.stderr)
    );

    let rewritten = fs::read_to_string(&manifest_path).expect("failed to read rewritten manifest");
    assert!(rewritten.contains("[package.metadata.feature-manifest.features]"));
    assert!(!rewritten.contains("legacy = { description = \"legacy\" }"));
    assert!(
        rewritten.contains("docs-preview = { description = \"TODO: describe `docs-preview`.\" }")
    );
}

#[test]
fn sync_diff_previews_rewrite_without_editing() {
    let temp_dir = copy_fixture_to_temp("basic");
    let manifest_path = temp_dir.path().join("Cargo.toml");

    let manifest = fs::read_to_string(&manifest_path).expect("failed to read temp manifest");
    let updated_manifest = manifest.replace(
        "unstable = { description = \"Experimental APIs; semver not guaranteed.\", unstable = true }\n",
        "",
    );
    fs::write(&manifest_path, updated_manifest.clone()).expect("failed to write temp manifest");

    let output = run_command(&[
        "sync",
        "--diff",
        "--manifest-path",
        manifest_path.to_str().expect("temp path should be UTF-8"),
    ]);

    assert!(!output.status.success());
    let stdout = normalize(&output.stdout);
    assert!(stdout.contains("sync drift in `feature-manifest-fixture`"));
    assert!(stdout.contains("--- a/"));
    assert!(stdout.contains("+unstable = { description = \"TODO: describe `unstable`.\" }"));

    let after = fs::read_to_string(&manifest_path).expect("failed to read temp manifest");
    assert_eq!(after, updated_manifest);
}

#[test]
fn markdown_can_write_and_inject_into_docs() {
    let temp_dir = copy_fixture_to_temp("basic");
    let manifest_path = temp_dir.path().join("Cargo.toml");
    let output_path = temp_dir.path().join("FEATURES.md");
    let readme_path = temp_dir.path().join("README.md");

    fs::write(
        &readme_path,
        "# Fixture\n\n<!-- feature-manifest:start -->\nold\n<!-- feature-manifest:end -->\n",
    )
    .expect("failed to write readme fixture");

    let output = run_command(&[
        "markdown",
        "--write",
        output_path.to_str().expect("output path should be UTF-8"),
        "--insert-into",
        readme_path.to_str().expect("readme path should be UTF-8"),
        "--manifest-path",
        manifest_path
            .to_str()
            .expect("manifest path should be UTF-8"),
    ]);

    assert!(
        output.status.success(),
        "stderr:\n{}",
        normalize(&output.stderr)
    );
    let written = fs::read_to_string(&output_path).expect("failed to read FEATURES.md");
    let injected = fs::read_to_string(&readme_path).expect("failed to read injected README");
    assert!(written.contains("# feature-manifest-fixture feature manifest"));
    assert!(injected.contains("<!-- feature-manifest:start -->"));
    assert!(injected.contains("Default feature set: `serde`"));
}

#[test]
fn markdown_check_detects_stale_files_and_injected_regions() {
    let temp_dir = copy_fixture_to_temp("basic");
    let manifest_path = temp_dir.path().join("Cargo.toml");
    let output_path = temp_dir.path().join("FEATURES.md");
    let readme_path = temp_dir.path().join("README.md");

    fs::write(&output_path, "stale\n").expect("failed to write stale FEATURES.md");
    fs::write(
        &readme_path,
        "# Fixture\n\n<!-- feature-manifest:start -->\nstale\n<!-- feature-manifest:end -->\n",
    )
    .expect("failed to write stale README");

    let stale = run_command(&[
        "md",
        "--check",
        "-o",
        output_path.to_str().expect("output path should be UTF-8"),
        "-i",
        readme_path.to_str().expect("readme path should be UTF-8"),
        "-m",
        manifest_path
            .to_str()
            .expect("manifest path should be UTF-8"),
    ]);
    assert!(!stale.status.success());
    assert!(normalize(&stale.stdout).contains("stale"));

    let rewrite = run_command(&[
        "md",
        "-o",
        output_path.to_str().expect("output path should be UTF-8"),
        "-i",
        readme_path.to_str().expect("readme path should be UTF-8"),
        "-m",
        manifest_path
            .to_str()
            .expect("manifest path should be UTF-8"),
    ]);
    assert!(rewrite.status.success());

    let fresh = run_command(&[
        "md",
        "--check",
        "-o",
        output_path.to_str().expect("output path should be UTF-8"),
        "-i",
        readme_path.to_str().expect("readme path should be UTF-8"),
        "-m",
        manifest_path
            .to_str()
            .expect("manifest path should be UTF-8"),
    ]);
    assert!(
        fresh.status.success(),
        "stderr:\n{}",
        normalize(&fresh.stderr)
    );

    let generated_file =
        fs::read_to_string(&output_path).expect("failed to read generated FEATURES.md");
    fs::write(&output_path, generated_file.replace('\n', "\r\n"))
        .expect("failed to rewrite FEATURES.md with CRLF line endings");
    let generated_readme =
        fs::read_to_string(&readme_path).expect("failed to read generated README");
    fs::write(&readme_path, generated_readme.replace('\n', "\r\n"))
        .expect("failed to rewrite README with CRLF line endings");

    let crlf_fresh = run_command(&[
        "md",
        "--check",
        "-o",
        output_path.to_str().expect("output path should be UTF-8"),
        "-i",
        readme_path.to_str().expect("readme path should be UTF-8"),
        "-m",
        manifest_path
            .to_str()
            .expect("manifest path should be UTF-8"),
    ]);
    assert!(
        crlf_fresh.status.success(),
        "stderr:\n{}",
        normalize(&crlf_fresh.stderr)
    );
}

#[test]
fn markdown_injection_rejects_duplicate_markers() {
    let temp_dir = copy_fixture_to_temp("basic");
    let manifest_path = temp_dir.path().join("Cargo.toml");
    let readme_path = temp_dir.path().join("README.md");

    fs::write(
        &readme_path,
        "<!-- feature-manifest:start -->\nold\n<!-- feature-manifest:start -->\n<!-- feature-manifest:end -->\n",
    )
    .expect("failed to write duplicate marker README");

    let output = run_command(&[
        "md",
        "-i",
        readme_path.to_str().expect("readme path should be UTF-8"),
        "-m",
        manifest_path
            .to_str()
            .expect("manifest path should be UTF-8"),
    ]);

    assert!(!output.status.success());
    assert!(normalize(&output.stderr).contains("duplicate feature-manifest markers"));
}

#[test]
fn short_markdown_alias_supports_short_flags() {
    let temp_dir = copy_fixture_to_temp("basic");
    let manifest_path = temp_dir.path().join("Cargo.toml");
    let output_path = temp_dir.path().join("FEATURES.md");

    let output = run_short_command(&[
        "md",
        "-o",
        output_path.to_str().expect("output path should be UTF-8"),
        "-m",
        manifest_path
            .to_str()
            .expect("manifest path should be UTF-8"),
    ]);

    assert!(
        output.status.success(),
        "stderr:\n{}",
        normalize(&output.stderr)
    );
    let written = fs::read_to_string(&output_path).expect("failed to read FEATURES.md");
    assert!(written.contains("# feature-manifest-fixture feature manifest"));
}

#[test]
fn init_scaffolds_metadata_readme_and_optional_ci() {
    let temp_dir = copy_fixture_to_temp("basic");
    let manifest_path = temp_dir.path().join("Cargo.toml");
    let readme_path = temp_dir.path().join("README.md");

    let manifest = fs::read_to_string(&manifest_path).expect("failed to read temp manifest");
    let stripped_manifest = manifest.replace(
        "unstable = { description = \"Experimental APIs; semver not guaranteed.\", unstable = true }\n",
        "",
    );
    fs::write(&manifest_path, stripped_manifest).expect("failed to write temp manifest");

    let output = run_command(&[
        "init",
        "--ci",
        "--readme",
        readme_path.to_str().expect("readme path should be UTF-8"),
        "-m",
        manifest_path
            .to_str()
            .expect("manifest path should be UTF-8"),
    ]);

    assert!(
        output.status.success(),
        "stderr:\n{}",
        normalize(&output.stderr)
    );

    let rewritten_manifest = fs::read_to_string(&manifest_path).expect("manifest should exist");
    let rewritten_readme = fs::read_to_string(&readme_path).expect("README should exist");
    let workflow = fs::read_to_string(
        temp_dir
            .path()
            .join(".github")
            .join("workflows")
            .join("feature-manifest.yml"),
    )
    .expect("workflow should exist");

    assert!(
        rewritten_manifest.contains("unstable = { description = \"TODO: describe `unstable`.\" }")
    );
    assert!(rewritten_readme.contains("<!-- feature-manifest:start -->"));
    assert!(rewritten_readme.contains("Default feature set: `serde`"));
    assert!(workflow.contains("cargo fm"));
    assert!(workflow.contains("actions/checkout@v6"));
}

#[test]
fn init_dry_run_reports_actions_without_writing() {
    let temp_dir = copy_fixture_to_temp("basic");
    let manifest_path = temp_dir.path().join("Cargo.toml");
    let readme_path = temp_dir.path().join("README.md");

    let manifest = fs::read_to_string(&manifest_path).expect("failed to read temp manifest");
    let stripped_manifest = manifest.replace(
        "unstable = { description = \"Experimental APIs; semver not guaranteed.\", unstable = true }\n",
        "",
    );
    fs::write(&manifest_path, stripped_manifest.clone()).expect("failed to write temp manifest");

    let output = run_command(&[
        "init",
        "--dry-run",
        "--ci",
        "--readme",
        readme_path.to_str().expect("readme path should be UTF-8"),
        "-m",
        manifest_path
            .to_str()
            .expect("manifest path should be UTF-8"),
    ]);

    assert!(
        output.status.success(),
        "stderr:\n{}",
        normalize(&output.stderr)
    );
    let stdout = normalize(&output.stdout);
    assert!(stdout.contains("would initialize metadata"));
    assert!(stdout.contains("would create README"));
    assert!(stdout.contains("would add CI workflow"));
    assert!(stdout.contains("dry run complete"));

    assert_eq!(
        fs::read_to_string(&manifest_path).expect("manifest should exist"),
        stripped_manifest
    );
    assert!(!readme_path.exists());
    assert!(
        !temp_dir
            .path()
            .join(".github")
            .join("workflows")
            .join("feature-manifest.yml")
            .exists()
    );
}

#[test]
fn doctor_reports_project_wiring() {
    let temp_dir = copy_fixture_to_temp("basic");
    let manifest_path = temp_dir.path().join("Cargo.toml");
    let readme_path = temp_dir.path().join("README.md");

    let init = run_command(&[
        "init",
        "--ci",
        "--readme",
        readme_path.to_str().expect("readme path should be UTF-8"),
        "-m",
        manifest_path
            .to_str()
            .expect("manifest path should be UTF-8"),
    ]);
    assert!(init.status.success());

    let output = run_command(&[
        "doctor",
        "--readme",
        readme_path.to_str().expect("readme path should be UTF-8"),
        "-m",
        manifest_path
            .to_str()
            .expect("manifest path should be UTF-8"),
    ]);

    assert!(
        output.status.success(),
        "stderr:\n{}",
        normalize(&output.stderr)
    );
    let stdout = normalize(&output.stdout);
    assert!(stdout.contains("feature metadata validates cleanly"));
    assert!(stdout.contains("README feature section is up to date"));
    assert!(stdout.contains("CI workflow references feature-manifest"));
}

#[test]
fn doctor_explain_prints_next_actions_for_findings() {
    let temp_dir = copy_fixture_to_temp("basic");
    let manifest_path = temp_dir.path().join("Cargo.toml");

    let output = run_command(&[
        "doctor",
        "--explain",
        "-m",
        manifest_path
            .to_str()
            .expect("manifest path should be UTF-8"),
    ]);

    assert!(
        output.status.success(),
        "stderr:\n{}",
        normalize(&output.stderr)
    );
    let stdout = normalize(&output.stdout);
    assert!(stdout.contains("warn: README markers were not found"));
    assert!(stdout.contains("next: run `cargo fm init --readme"));
}

#[test]
fn doctor_strict_fails_on_warnings() {
    let temp_dir = copy_fixture_to_temp("basic");
    let manifest_path = temp_dir.path().join("Cargo.toml");

    let output = run_command(&[
        "doctor",
        "--strict",
        "-m",
        manifest_path
            .to_str()
            .expect("manifest path should be UTF-8"),
    ]);

    assert!(!output.status.success());
    assert!(normalize(&output.stderr).contains("doctor found warnings in strict mode"));
}

#[test]
fn schema_and_completions_do_not_require_manifest() {
    let temp_dir = tempfile::tempdir().expect("failed to create temp dir");

    let schema = run_short_command_in(temp_dir.path(), &["schema", "metadata"]);
    assert!(
        schema.status.success(),
        "stderr:\n{}",
        normalize(&schema.stderr)
    );
    assert!(normalize(&schema.stdout).contains("\"title\": \"feature-manifest metadata report\""));

    let completions = run_short_command_in(temp_dir.path(), &["completions", "powershell"]);
    assert!(
        completions.status.success(),
        "stderr:\n{}",
        normalize(&completions.stderr)
    );
    assert!(normalize(&completions.stdout).contains("cargo-fm"));
}

#[test]
fn edge_fixture_covers_mixed_layout_and_dependency_shapes() {
    let manifest_path = fixture_path("edge");
    let output = run_command(&[
        "check",
        "-m",
        manifest_path
            .to_str()
            .expect("fixture path should be UTF-8"),
    ]);

    assert!(
        output.status.success(),
        "stderr:\n{}",
        normalize(&output.stderr)
    );
}
