//! Integration tests — snapshot-verify the M3 caixa renderer
//! against the 6 example .caixa.lisp sources in substrate.
//!
//! Each test renders ONE ecosystem's caixa + asserts the
//! generated artifacts contain the expected typed content. If a
//! renderer regresses, these tests catch it before merge.

use std::path::PathBuf;
use std::process::Command;

/// Path to the bundled example caixa sources.
fn example_source(ecosystem: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures")
        .join(format!("{ecosystem}.caixa.lisp"))
}

/// Render a caixa source via the actual binary + return the tmp dir path.
fn render_to_tmp(source: &PathBuf) -> tempdir::TempDir {
    let tmp = tempdir::TempDir::new("plg-snapshot").expect("tempdir");
    let bin = env!("CARGO_BIN_EXE_pleme-doc-gen");
    let status = Command::new(bin)
        .args([
            "caixa",
            "--source",
            source.to_str().unwrap(),
            "--out",
            tmp.path().to_str().unwrap(),
            "--force",
        ])
        .status()
        .expect("run pleme-doc-gen");
    assert!(status.success(), "pleme-doc-gen failed for {source:?}");
    tmp
}

fn read_artifact(dir: &PathBuf, rel: &str) -> String {
    std::fs::read_to_string(dir.join(rel)).unwrap_or_else(|_| format!("(missing {rel})"))
}

#[test]
fn rust_single_crate_renders_cargo_toml_with_metadata() {
    let src = example_source("rust-single-crate");
    let tmp = render_to_tmp(&src);
    let cargo = read_artifact(&tmp.path().to_path_buf(), "Cargo.toml");
    assert!(cargo.contains("[package]"), "missing [package] section");
    assert!(cargo.contains("name = \"test-rust-lib\""), "missing name");
    assert!(cargo.contains("version = \"0.1.0\""), "missing version");
    assert!(cargo.contains("license = \"MIT\""), "missing license");
    assert!(cargo.contains("edition = \"2024\""), "missing edition");
}

#[test]
fn rust_single_crate_renders_ci_shims() {
    let src = example_source("rust-single-crate");
    let tmp = render_to_tmp(&src);
    let auto_release = read_artifact(
        &tmp.path().to_path_buf(),
        ".github/workflows/auto-release.yml",
    );
    assert!(auto_release.contains("uses: pleme-io/substrate"), "wrong reusable ref");
    assert!(auto_release.contains("auto-release.yml@main"), "missing dispatcher pointer");
}

#[test]
fn rust_single_crate_renders_config_toml() {
    let src = example_source("rust-single-crate");
    let tmp = render_to_tmp(&src);
    let cfg = read_artifact(&tmp.path().to_path_buf(), ".pleme-io-release.toml");
    assert!(cfg.contains("[bump]"), "missing [bump] section");
    assert!(cfg.contains("default-type = \"patch\""), "missing default-type");
}

#[test]
fn npm_renders_package_json() {
    let src = example_source("npm");
    let tmp = render_to_tmp(&src);
    let pkg = read_artifact(&tmp.path().to_path_buf(), "package.json");
    assert!(pkg.contains("\"name\":"), "missing name field");
    assert!(pkg.contains("\"version\":"), "missing version field");
    assert!(pkg.contains("\"license\":"), "missing license field");
}

#[test]
fn python_renders_pyproject_toml() {
    let src = example_source("python");
    let tmp = render_to_tmp(&src);
    let py = read_artifact(&tmp.path().to_path_buf(), "pyproject.toml");
    assert!(py.contains("[project]"), "missing [project] section");
    assert!(py.contains("name = "), "missing name");
    assert!(py.contains("requires-python"), "missing requires-python");
}

#[test]
fn helm_renders_chart_yaml() {
    let src = example_source("helm");
    let tmp = render_to_tmp(&src);
    let chart = read_artifact(&tmp.path().to_path_buf(), "Chart.yaml");
    assert!(chart.contains("apiVersion: v2"), "missing apiVersion");
    assert!(chart.contains("type: "), "missing type");
}

#[test]
fn github_action_renders_action_yml_and_run_tlisp() {
    let src = example_source("github-action");
    let tmp = render_to_tmp(&src);
    let action = read_artifact(&tmp.path().to_path_buf(), "action.yml");
    assert!(action.contains("using: composite"), "missing composite shape");
    assert!(action.contains("tatara-script"), "missing tatara-script invocation");
    let run = read_artifact(&tmp.path().to_path_buf(), "run.tlisp");
    assert!(!run.is_empty(), "run.tlisp empty");
}

#[test]
fn rust_workspace_renders_workspace_cargo_toml() {
    let src = example_source("rust-workspace");
    let tmp = render_to_tmp(&src);
    let cargo = read_artifact(&tmp.path().to_path_buf(), "Cargo.toml");
    assert!(cargo.contains("[workspace]"), "missing [workspace]");
    assert!(cargo.contains("resolver = \"2\""), "missing resolver");
}

#[test]
fn nix_module_trio_emitted_by_default() {
    let src = example_source("rust-single-crate");
    let tmp = render_to_tmp(&src);
    let root = tmp.path().to_path_buf();
    assert!(root.join("nix/modules/nixos.nix").exists(), "missing nixos.nix");
    assert!(root.join("nix/modules/darwin.nix").exists(), "missing darwin.nix");
    assert!(root.join("nix/modules/home-manager.nix").exists(), "missing home-manager.nix");
    let nixos = read_artifact(&root, "nix/modules/nixos.nix");
    assert!(nixos.contains("config.services."), "nixos module missing services namespace");
    assert!(nixos.contains("lib.mkEnableOption"), "nixos module missing mkEnableOption");
}

#[test]
fn flake_nix_emitted_with_substrate_input() {
    let src = example_source("rust-single-crate");
    let tmp = render_to_tmp(&src);
    let flake = read_artifact(&tmp.path().to_path_buf(), "flake.nix");
    assert!(flake.contains("substrate = {"), "missing substrate input");
    assert!(flake.contains("flake-parts"), "missing flake-parts");
    assert!(flake.contains("rustToolReleaseFlakeBuilder"), "wrong builder for rust-single-crate");
    assert!(flake.contains("packages.default ="), "missing packages.default");
}

#[test]
fn flake_nix_npm_uses_typescript_builder() {
    let src = example_source("npm");
    let tmp = render_to_tmp(&src);
    let flake = read_artifact(&tmp.path().to_path_buf(), "flake.nix");
    assert!(flake.contains("typescriptToolFlakeBuilder"), "wrong builder for npm");
}

#[test]
fn flake_nix_python_uses_python_builder() {
    let src = example_source("python");
    let tmp = render_to_tmp(&src);
    let flake = read_artifact(&tmp.path().to_path_buf(), "flake.nix");
    assert!(flake.contains("pythonToolFlakeBuilder"), "wrong builder for python");
}

#[test]
fn features_block_emitted() {
    let src = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/features-rich.caixa.lisp");
    let tmp = render_to_tmp(&src);
    let cargo = read_artifact(&tmp.path().to_path_buf(), "Cargo.toml");
    assert!(cargo.contains("[features]"), "missing [features]");
    assert!(cargo.contains("default = [\"serde-support\"]"), "missing default feature");
    assert!(cargo.contains("serde-support = [\"serde\", \"serde_json\"]"), "missing serde-support deps");
    assert!(cargo.contains("async = [\"tokio\", \"futures\"]"), "missing async deps");
}

#[test]
fn profiles_blocks_emitted() {
    let src = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/features-rich.caixa.lisp");
    let tmp = render_to_tmp(&src);
    let cargo = read_artifact(&tmp.path().to_path_buf(), "Cargo.toml");
    assert!(cargo.contains("[profile.release]"), "missing release profile");
    assert!(cargo.contains("lto = true"), "missing lto");
    assert!(cargo.contains("opt-level = 3"), "missing opt-level");
    assert!(cargo.contains("[profile.dev]"), "missing dev profile");
}

#[test]
fn publish_include_emitted() {
    let src = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/features-rich.caixa.lisp");
    let tmp = render_to_tmp(&src);
    let cargo = read_artifact(&tmp.path().to_path_buf(), "Cargo.toml");
    assert!(cargo.contains("include = ["), "missing publish.files-include emission");
}

// ─── M4.4 — typed xml_ast emitters ──────────────────────────────────

#[test]
fn maven_renders_typed_pom_xml() {
    let src = example_source("maven");
    let tmp = render_to_tmp(&src);
    let pom = read_artifact(&tmp.path().to_path_buf(), "pom.xml");
    assert!(pom.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"),
        "missing XML declaration");
    assert!(pom.contains("<groupId>io.pleme.demo</groupId>"), "missing groupId");
    assert!(pom.contains("<artifactId>demo-maven</artifactId>"), "missing artifactId");
    assert!(pom.contains("<version>1.2.3</version>"), "missing version");
    assert!(pom.contains("<license>"), "missing license block");
    assert!(pom.contains("<dependency>"), "missing dependency block");
    assert!(pom.contains("<scope>test</scope>"), "missing test scope on dev dep");
    // Typed AST guarantees: no broken indent, no unescaped chars.
    assert!(!pom.contains("&lt;script"), "should not have leaked raw tags");
}

#[test]
fn csproj_renders_typed_project_xml() {
    let src = example_source("csproj");
    let tmp = render_to_tmp(&src);
    let proj = read_artifact(&tmp.path().to_path_buf(), "demo-net.csproj");
    assert!(proj.contains("<Project Sdk=\"Microsoft.NET.Sdk\">"), "missing Project root");
    assert!(proj.contains("<TargetFramework>net8.0</TargetFramework>"), "missing TargetFramework");
    assert!(proj.contains("<OutputType>Library</OutputType>"), "missing OutputType");
    assert!(proj.contains("<PackageReference"), "missing PackageReference");
    assert!(proj.contains("PrivateAssets=\"all\""), "missing PrivateAssets on dev dep");
    // csproj has NO `<?xml ... ?>` declaration by convention.
    assert!(!proj.starts_with("<?xml"), "csproj should not carry XML declaration");
}

// ─── M4.5 — typed sexp_ast emitters ─────────────────────────────────

#[test]
fn dune_renders_typed_sexp() {
    let src = example_source("dune");
    let tmp = render_to_tmp(&src);
    let dune = read_artifact(&tmp.path().to_path_buf(), "dune-project");
    assert!(dune.contains("(lang dune 3.14)"), "missing lang form");
    assert!(dune.contains("(name demo-dune)"), "missing name form");
    assert!(dune.contains("(generate_opam_files true)"), "missing generate_opam_files");
    assert!(dune.contains("(source"), "missing source form");
    assert!(dune.contains("(github pleme-io/demo-dune)"), "missing github source");
    assert!(dune.contains("(authors \"pleme-io\")"), "missing authors string-form");
    assert!(dune.contains("(ocaml"), "missing ocaml dep form");
    assert!(dune.contains("(>= 5.1)"), "supports->ocaml >= req not flowed");
    assert!(dune.contains("(alcotest :with-test)"), "dev dep missing with-test keyword");
}
