use std::{
    fs,
    path::{Path, PathBuf},
    process::{Command, Output},
    sync::atomic::{AtomicUsize, Ordering},
};

static NEXT_FIXTURE_ID: AtomicUsize = AtomicUsize::new(0);

#[test]
fn local_canonical_fixture_is_formatter_stable() {
    let compiler = env!("CARGO_BIN_EXE_glagol");
    let fixture = Path::new("../tests/canonical.fmt");
    let expected = fs::read_to_string("../tests/canonical.fmt").expect("read canonical.fmt");

    let output = run_formatter(compiler, fixture);

    assert_success(&output);
    assert_eq!(
        String::from_utf8(output.stdout).expect("formatter output is UTF-8"),
        expected,
    );
    assert!(
        output.stderr.is_empty(),
        "formatter wrote stderr:\n{}",
        String::from_utf8_lossy(&output.stderr),
    );
}

#[test]
fn local_top_level_test_fixture_is_formatter_stable() {
    let compiler = env!("CARGO_BIN_EXE_glagol");
    let fixture = Path::new("../tests/top-level-test.fmt");
    let expected =
        fs::read_to_string("../tests/top-level-test.fmt").expect("read top-level-test.fmt");

    let output = run_formatter(compiler, fixture);

    assert_success(&output);
    assert_eq!(
        String::from_utf8(output.stdout).expect("formatter output is UTF-8"),
        expected,
    );
    assert!(
        output.stderr.is_empty(),
        "formatter wrote stderr:\n{}",
        String::from_utf8_lossy(&output.stderr),
    );
}

#[test]
fn local_v1_formatter_stability_fixture_is_formatter_stable() {
    let compiler = env!("CARGO_BIN_EXE_glagol");
    let fixture = Path::new("../tests/formatter-stability-v1.fmt");
    let expected = fs::read_to_string("../tests/formatter-stability-v1.fmt")
        .expect("read formatter-stability-v1.fmt");

    let output = run_formatter(compiler, fixture);

    assert_success(&output);
    assert_eq!(
        String::from_utf8(output.stdout).expect("formatter output is UTF-8"),
        expected,
    );
    assert!(
        output.stderr.is_empty(),
        "formatter wrote stderr:\n{}",
        String::from_utf8_lossy(&output.stderr),
    );
}

#[test]
fn local_comments_fixture_is_formatter_stable() {
    let compiler = env!("CARGO_BIN_EXE_glagol");
    let fixture = Path::new("../tests/comments.slo");
    let expected = fs::read_to_string("../tests/comments.slo").expect("read comments.slo");

    let output = run_formatter(compiler, fixture);

    assert_success(&output);
    assert_eq!(
        String::from_utf8(output.stdout).expect("formatter output is UTF-8"),
        expected,
    );
    assert!(
        output.stderr.is_empty(),
        "formatter wrote stderr:\n{}",
        String::from_utf8_lossy(&output.stderr),
    );
}

#[test]
fn formatter_canonicalizes_supported_syntax() {
    let compiler = env!("CARGO_BIN_EXE_glagol");
    let fixture = write_fixture(
        "supported-messy",
        r#"; status: formatter-canonical
   ; Scope: current strict supported syntax only.
(module   main)
(fn add (
 (a i32)
 (b i32)) -> i32 (+ a b))

(fn main() -> i32
(print_i32(add 20 22))
0)
"#,
    );
    let expected = fs::read_to_string("../tests/canonical.fmt").expect("read canonical.fmt");

    let output = run_formatter(compiler, &fixture);

    assert_success(&output);
    assert_eq!(
        String::from_utf8(output.stdout).expect("formatter output is UTF-8"),
        expected,
    );
}

#[test]
fn formatter_canonicalizes_struct_forms() {
    let compiler = env!("CARGO_BIN_EXE_glagol");
    let fixture = write_fixture(
        "struct",
        r#"
(module main)

(struct   Point (x   i32) (y i32))

(fn point_sum () -> i32
  (+ (. (Point (x 20) (y 22)) x) (. (Point (x 20) (y 22)) y)))

(fn main () -> i32
  (point_sum))
"#,
    );
    let expected = concat!(
        "(module main)\n",
        "\n",
        "(struct Point\n",
        "  (x i32)\n",
        "  (y i32))\n",
        "\n",
        "(fn point_sum () -> i32\n",
        "  (+ (. (Point (x 20) (y 22)) x) (. (Point (x 20) (y 22)) y)))\n",
        "\n",
        "(fn main () -> i32\n",
        "  (point_sum))\n",
    );

    let output = run_formatter(compiler, &fixture);

    assert_success(&output);
    assert_eq!(
        String::from_utf8(output.stdout).expect("formatter output is UTF-8"),
        expected,
    );
}

#[test]
fn formatter_canonicalizes_top_level_tests() {
    let compiler = env!("CARGO_BIN_EXE_glagol");
    let fixture = write_fixture(
        "top-level-test",
        r#"; status: formatter-canonical
   ; Scope: promoted top-level test formatter contract.
(module   tests)

(fn add (
 (a i32)
 (b i32)) -> i32 (+ a b))

(test   "add works" (= (add 2 3) 5))

(fn main() -> i32 0)
"#,
    );
    let expected =
        fs::read_to_string("../tests/top-level-test.fmt").expect("read top-level-test.fmt");

    let output = run_formatter(compiler, &fixture);

    assert_success(&output);
    assert_eq!(
        String::from_utf8(output.stdout).expect("formatter output is UTF-8"),
        expected,
    );
}

#[test]
fn formatter_keeps_long_inline_forms_inline() {
    let compiler = env!("CARGO_BIN_EXE_glagol");
    let fixture = write_fixture(
        "long-inline",
        r#"
(module main)

(fn accept_many (
  (a i32)
  (b i32)
  (c i32)
  (d i32)
  (e i32)
  (f i32)
  (g i32)
  (h i32)
  (i i32)
  (j i32)) -> i32
  (+ a j))

(fn main () -> i32
  (accept_many
    100000001
    100000002
    100000003
    100000004
    100000005
    100000006
    100000007
    100000008
    100000009
    100000010))
"#,
    );
    let expected = concat!(
        "(module main)\n",
        "\n",
        "(fn accept_many ((a i32) (b i32) (c i32) (d i32) (e i32) (f i32) (g i32) (h i32) (i i32) (j i32)) -> i32\n",
        "  (+ a j))\n",
        "\n",
        "(fn main () -> i32\n",
        "  (accept_many 100000001 100000002 100000003 100000004 100000005 100000006 100000007 100000008 100000009 100000010))\n",
    );

    let output = run_formatter(compiler, &fixture);

    assert_success(&output);
    assert_eq!(
        String::from_utf8(output.stdout).expect("formatter output is UTF-8"),
        expected,
    );
}

#[test]
fn formatter_rejects_test_names_outside_v0_subset() {
    let compiler = env!("CARGO_BIN_EXE_glagol");
    let fixture = write_fixture(
        "invalid-test-name",
        r#"
(module main)

(test "bad\nname"
  true)
"#,
    );

    let output = run_formatter(compiler, &fixture);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        !output.status.success(),
        "formatter unexpectedly accepted invalid test name\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr,
    );
    assert!(
        stdout.is_empty(),
        "formatter emitted stdout for rejected test name\nstdout:\n{}\nstderr:\n{}",
        stdout,
        stderr,
    );
    assert!(
        stderr.contains("error[InvalidTestName]") || stderr.contains("(code InvalidTestName)"),
        "stderr did not contain InvalidTestName\nstderr:\n{}",
        stderr,
    );
}

#[test]
fn formatter_reports_unsupported_standard_library_calls() {
    let compiler = env!("CARGO_BIN_EXE_glagol");
    let cases = [
        (
            "unsupported-std-call",
            r#"
(module main)

(fn main () -> i32
  (std.io.print_unit 0)
  0)
"#,
        ),
        (
            "unsupported-std-user-call",
            r#"
(module main)

(fn std.io.print_unit ((value i32)) -> i32
  value)

(fn main () -> i32
  (std.io.print_unit 0))
"#,
        ),
    ];

    for (name, source) in cases {
        let fixture = write_fixture(name, source);
        let output = run_formatter(compiler, &fixture);
        let stdout = String::from_utf8_lossy(&output.stdout);
        let stderr = String::from_utf8_lossy(&output.stderr);

        assert!(
            !output.status.success(),
            "formatter unexpectedly accepted unsupported std call `{}`\nstdout:\n{}\nstderr:\n{}",
            name,
            stdout,
            stderr,
        );
        assert!(
            stdout.is_empty(),
            "formatter emitted stdout for unsupported std call `{}`\nstdout:\n{}\nstderr:\n{}",
            name,
            stdout,
            stderr,
        );
        assert!(
            stderr.contains("UnsupportedStandardLibraryCall"),
            "formatter stderr did not contain UnsupportedStandardLibraryCall for `{}`\nstderr:\n{}",
            name,
            stderr,
        );
    }
}

#[test]
fn formatter_rejects_reserved_generic_collection_syntax() {
    let compiler = env!("CARGO_BIN_EXE_glagol");
    let cases = [
        (
            "generic-function",
            r#"
(module main)

(fn id (type_params T) ((value T)) -> T
  value)
"#,
            "UnsupportedGenericFunction",
        ),
        (
            "generic-type-alias",
            r#"
(module main)

(type VecOf (type_params T) (vec T))
"#,
            "UnsupportedGenericTypeAlias",
        ),
        (
            "generic-type-parameter",
            r#"
(module main)

(fn main () -> i32
  (let xs (vec T) (std.vec.i32.empty))
  0)
"#,
            "UnsupportedGenericTypeParameter",
        ),
        (
            "map-type",
            r#"
(module main)

(fn main () -> (map string i32)
  0)
"#,
            "UnsupportedMapType",
        ),
        (
            "set-type",
            r#"
(module main)

(fn main () -> (set string)
  0)
"#,
            "UnsupportedSetType",
        ),
        (
            "generic-std-call",
            r#"
(module main)

(fn main () -> i32
  (std.vec.empty i32)
  0)
"#,
            "UnsupportedGenericStandardLibraryCall",
        ),
    ];

    for (name, source, code) in cases {
        let fixture = write_fixture(name, source);
        let output = run_formatter(compiler, &fixture);
        let stdout = String::from_utf8_lossy(&output.stdout);
        let stderr = String::from_utf8_lossy(&output.stderr);

        assert!(
            !output.status.success(),
            "formatter unexpectedly accepted reserved generic syntax `{}`\nstdout:\n{}\nstderr:\n{}",
            name,
            stdout,
            stderr,
        );
        assert!(
            stdout.is_empty(),
            "formatter emitted stdout for reserved generic syntax `{}`\nstdout:\n{}\nstderr:\n{}",
            name,
            stdout,
            stderr,
        );
        assert!(
            stderr.contains(code),
            "formatter stderr did not contain {} for `{}`\nstderr:\n{}",
            code,
            name,
            stderr,
        );
    }
}

#[test]
fn formatter_preserves_full_line_comments_inside_function_bodies() {
    let compiler = env!("CARGO_BIN_EXE_glagol");
    let fixture = write_fixture(
        "body-comments",
        r#"
(module main)

(fn add ((a i32) (b i32)) -> i32
  ; keep this in add
  (+ a b))

(fn main () -> i32
  ; before effect
  (print_i32 (add 20 21))
  ; before result
  (+ 20 22)
  ; after result
)
"#,
    );

    let output = run_formatter(compiler, &fixture);

    assert_success(&output);
    assert_eq!(
        String::from_utf8(output.stdout).expect("formatter output is UTF-8"),
        concat!(
            "(module main)\n",
            "\n",
            "(fn add ((a i32) (b i32)) -> i32\n",
            "  ; keep this in add\n",
            "  (+ a b))\n",
            "\n",
            "(fn main () -> i32\n",
            "  ; before effect\n",
            "  (print_i32 (add 20 21))\n",
            "  ; before result\n",
            "  (+ 20 22)\n",
            "  ; after result\n",
            ")\n",
        ),
    );
}

#[test]
fn formatter_preserves_full_line_comments_after_last_form() {
    let compiler = env!("CARGO_BIN_EXE_glagol");
    let fixture = write_fixture(
        "trailing-comments",
        r#"
(module main)

(fn main () -> i32
  0)

; keep this file trailer
  ; and normalize indentation
"#,
    );

    let output = run_formatter(compiler, &fixture);

    assert_success(&output);
    assert_eq!(
        String::from_utf8(output.stdout).expect("formatter output is UTF-8"),
        concat!(
            "(module main)\n",
            "\n",
            "(fn main () -> i32\n",
            "  0)\n",
            "\n",
            "; keep this file trailer\n",
            "; and normalize indentation\n",
        ),
    );
}

#[test]
fn formatter_rejects_comments_inside_inline_expression_forms() {
    let compiler = env!("CARGO_BIN_EXE_glagol");
    let fixture = write_fixture(
        "expression-comment",
        r#"
(module main)

(fn main () -> i32
  (+ 20
     ; cannot keep this while preserving inline calls
     22))
"#,
    );

    let output = run_formatter(compiler, &fixture);
    assert_formatter_comment_rejection(&output, "inline expression form");
}

#[test]
fn formatter_rejects_same_line_and_trailing_comments() {
    let compiler = env!("CARGO_BIN_EXE_glagol");
    let cases = [
        (
            "same-line-module-comment",
            r#"
(module main) ; unsupported trailing module comment

(fn main () -> i32
  0)
"#,
            "same-line module comment",
        ),
        (
            "same-line-expression-comment",
            r#"
(module main)

(fn main () -> i32
  (+ 20 22) ; unsupported trailing expression comment
)
"#,
            "same-line expression comment",
        ),
    ];

    for (name, source, label) in cases {
        let fixture = write_fixture(name, source);
        let output = run_formatter(compiler, &fixture);
        assert_formatter_comment_rejection(&output, label);
    }
}

#[test]
fn formatter_rejects_comments_in_headers_and_signatures() {
    let compiler = env!("CARGO_BIN_EXE_glagol");
    let cases = [
        (
            "module-header-comment",
            r#"
(module
  ; unsupported module header comment
  main)
"#,
            "module header",
        ),
        (
            "struct-signature-comment",
            r#"
(module main)

(struct
  ; unsupported struct signature comment
  Pair
  (left i32)
  (right i32))
"#,
            "struct signature",
        ),
        (
            "function-signature-comment",
            r#"
(module main)

(fn add ((a i32)
  ; unsupported function signature comment
  (b i32)) -> i32
  (+ a b))
"#,
            "function signature",
        ),
        (
            "test-header-comment",
            r#"
(module main)

(test
  ; unsupported test header comment
  "addition works"
  true)
"#,
            "test header",
        ),
    ];

    for (name, source, label) in cases {
        let fixture = write_fixture(name, source);
        let output = run_formatter(compiler, &fixture);
        assert_formatter_comment_rejection(&output, label);
    }
}

fn run_formatter(compiler: &str, fixture: &Path) -> Output {
    Command::new(compiler)
        .arg("--format")
        .arg(fixture)
        .output()
        .unwrap_or_else(|err| panic!("run glagol --format on `{}`: {}", fixture.display(), err))
}

fn assert_success(output: &Output) {
    assert!(
        output.status.success(),
        "formatter failed\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr),
    );
}

fn assert_formatter_comment_rejection(output: &Output, label: &str) {
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        !output.status.success(),
        "formatter unexpectedly accepted unsupported comment position in {}\nstdout:\n{}\nstderr:\n{}",
        label,
        stdout,
        stderr,
    );
    assert!(
        stdout.is_empty(),
        "formatter emitted stdout for rejected {}\nstdout:\n{}\nstderr:\n{}",
        label,
        stdout,
        stderr,
    );
    assert!(
        stderr.contains("error[UnsupportedFormatterComment]"),
        "stderr did not contain human UnsupportedFormatterComment for {}\nstderr:\n{}",
        label,
        stderr,
    );
    assert!(
        stderr.contains("  (schema slovo.diagnostic)\n")
            && stderr.contains("  (version 1)\n")
            && stderr.contains("  (code UnsupportedFormatterComment)\n")
            && stderr.contains("  (span\n"),
        "stderr did not contain structured UnsupportedFormatterComment diagnostic for {}\nstderr:\n{}",
        label,
        stderr,
    );
}

fn write_fixture(name: &str, source: &str) -> PathBuf {
    let mut path = std::env::temp_dir();
    let id = NEXT_FIXTURE_ID.fetch_add(1, Ordering::Relaxed);
    path.push(format!(
        "glagol-formatter-{}-{}-{}.slo",
        std::process::id(),
        id,
        name
    ));
    fs::write(&path, source).unwrap_or_else(|err| panic!("write `{}`: {}", path.display(), err));
    path
}
