//! End-to-end CFG lowering invariants over C snippets.

use std::path::PathBuf;
use std::sync::Arc;

use rcc_cfg::{
    build_bodies, pretty::dump_body, verify::verify_body_with_hir, BasicBlockId, Body, CastKind,
    Const, ConstKind, InlineAsmArg, Operand, Place, Projection, Rvalue, StatementKind,
    TerminatorKind,
};
use rcc_errors::{CaptureEmitter, Handler};
use rcc_hir::{DefId, HirCrate, Local, Ty, TyCtxt, TyId};
use rcc_hir_lower::lower;
use rcc_session::{LanguageStandard, Options, Session};
use rcc_typeck::{check, verify_typed_hir};

struct Fixture {
    name: &'static str,
    src: &'static str,
    functions: usize,
}

struct Lowered {
    tcx: TyCtxt,
    hir: HirCrate,
    bodies: Vec<(DefId, Body)>,
}

struct EdgeFixture {
    name: &'static str,
    task: &'static str,
    review_finding: &'static str,
    src: &'static str,
    functions: usize,
}

const FIXTURES: &[Fixture] = &[
    Fixture { name: "return_zero", src: "int f(void) { return 0; }", functions: 1 },
    Fixture { name: "return_param", src: "int f(int x) { return x; }", functions: 1 },
    Fixture {
        name: "local_arithmetic",
        src: "int f(int a, int b) { int x = a + b * 2; return x; }",
        functions: 1,
    },
    Fixture {
        name: "comma_expression",
        src: "int f(void) { int x = 1; int y = (x = 2, x + 3); return y; }",
        functions: 1,
    },
    Fixture {
        name: "casts",
        src: "int f(double d) { int x = (int)d; return x; }",
        functions: 1,
    },
    Fixture {
        name: "logical_and",
        src: "int f(int a, int b) { return a && b; }",
        functions: 1,
    },
    Fixture {
        name: "logical_or",
        src: "int f(int a, int b) { return a || b; }",
        functions: 1,
    },
    Fixture {
        name: "conditional_expression",
        src: "int f(int a) { return a ? 1 : 2; }",
        functions: 1,
    },
    Fixture {
        name: "if_without_else",
        src: "int f(int a) { int x = 0; if (a) x = 1; return x; }",
        functions: 1,
    },
    Fixture {
        name: "if_else",
        src: "int f(int a) { int x = 0; if (a) x = 1; else x = 2; return x; }",
        functions: 1,
    },
    Fixture {
        name: "nested_if",
        src: "int f(int a, int b) { int x = 0; if (a) { if (b) x = 1; } return x; }",
        functions: 1,
    },
    Fixture {
        name: "while_loop",
        src: "int f(int n) { int i = 0; while (i < n) { i = i + 1; } return i; }",
        functions: 1,
    },
    Fixture {
        name: "do_while_loop",
        src: "int f(int n) { int i = 0; do { i = i + 1; } while (i < n); return i; }",
        functions: 1,
    },
    Fixture {
        name: "for_expression_init",
        src: "int f(int n) { int i = 0; for (i = 0; i < n; i = i + 1) {} return i; }",
        functions: 1,
    },
    Fixture {
        name: "for_prefix_increment_step",
        src: "int f(int n) { int i = 0; for (i = 0; i < n; ++i) {} return i; }",
        functions: 1,
    },
    Fixture {
        name: "postfix_increment_return",
        src: "int f(void) { int i = 0; return i++; }",
        functions: 1,
    },
    Fixture {
        name: "prefix_decrement_return",
        src: "int f(void) { int i = 2; return --i; }",
        functions: 1,
    },
    Fixture {
        name: "deref_postfix_increment",
        src: "int f(int *p) { return (*p)++; }",
        functions: 1,
    },
    Fixture {
        name: "postfix_pointer_deref",
        src: "int f(int *p) { return *p++; }",
        functions: 1,
    },
    Fixture {
        name: "array_index_postfix_increment",
        src: "int f(int i) { int a[2] = {1, 2}; return a[i]++; }",
        functions: 1,
    },
    Fixture {
        name: "for_infinite_with_break",
        src: "int f(void) { int i = 0; for (;;) { i = i + 1; if (i == 3) break; } return i; }",
        functions: 1,
    },
    Fixture {
        name: "break_continue",
        src: "int f(int n) { int i = 0; while (i < n) { i = i + 1; if (i == 2) continue; if (i == 4) break; } return i; }",
        functions: 1,
    },
    Fixture {
        name: "switch_cases",
        src: "int f(int x) { int y = 0; switch (x) { case 1: y = 2; break; case 2: y = 3; default: y = 4; } return y; }",
        functions: 1,
    },
    Fixture {
        name: "goto_label",
        src: "int f(int x) { int y = 0; if (x) goto L; y = 1; L: return y; }",
        functions: 1,
    },
    Fixture {
        name: "array_init_index",
        src: "int f(void) { int a[3] = {1, 2}; return a[1]; }",
        functions: 1,
    },
    Fixture {
        name: "struct_field",
        src: "struct S { int a; int b; }; int f(void) { struct S s = {1, 2}; return s.b; }",
        functions: 1,
    },
    Fixture {
        name: "address_and_deref",
        src: "int f(void) { int x = 1; int *p = &x; return *p; }",
        functions: 1,
    },
    Fixture {
        name: "call_non_void",
        src: "int g(int); int f(int x) { return g(x); }",
        functions: 1,
    },
    Fixture {
        name: "call_void",
        src: "void g(int); int f(int x) { g(x); return x; }",
        functions: 1,
    },
    Fixture {
        name: "variadic_call",
        src: "int printf(char *, ...); int f(void) { printf(\"%d\", 1); return 0; }",
        functions: 1,
    },
    Fixture {
        name: "vla_sizeof",
        src: "unsigned long f(int n) { int a[n]; return sizeof a; }",
        functions: 1,
    },
    Fixture {
        name: "sizeof_int_array",
        src: "unsigned long f(void) { int a[3]; return sizeof a; }",
        functions: 1,
    },
    Fixture {
        name: "real_to_complex_return",
        src: "double _Complex f(double x) { return x; }",
        functions: 1,
    },
    Fixture {
        name: "complex_to_real_return",
        src: "double f(double _Complex z) { return z; }",
        functions: 1,
    },
    Fixture {
        name: "multi_function",
        src: "int a(void) { return 1; } int b(void) { return a(); }",
        functions: 2,
    },
    Fixture {
        name: "block_scope",
        src: "int f(void) { int x = 1; { int y = 2; x = x + y; } return x; }",
        functions: 1,
    },
];

// Former CFG review findings guarded by the source-level edge matrix:
//
// - 08-16: inc/dec lowering must survive parser/typeck shapes, including
//   declaration-style `for` initializers and postfix result values.
// - 08-17: goto lifetime fixups must emit scope exits when jumping out of
//   nested scopes and must allow jumps around, not into, VLA scopes.
// - 08-18: sizeof lowering must use the shared layout service for records and
//   record-element VLAs, not ad hoc scalar sizes.
// - 08-19: complex conversions must remain explicit CFG rvalues after the full
//   source pipeline inserts typeck conversions.
// - 08-20: every edge case is fed through verify_body after build_bodies.
// - 08-21: source-level order fixtures assert MIR order only; they never depend
//   on a runtime result for unspecified operand order.
// - 11-16f: dead conditional arms that contain statement expressions may
//   terminate themselves; CFG lowering must not add a second goto afterwards.
const EDGE_FIXTURES: &[EdgeFixture] = &[
    EdgeFixture {
        name: "edge_for_decl_init_prefix_inc",
        task: "08-16",
        review_finding: "for-init declarations plus prefix increment previously only had hand-built HIR coverage",
        src: "int f(int n) { int total = 0; for (int i = 0; i < n; ++i) { total++; } return total; }",
        functions: 1,
    },
    EdgeFixture {
        name: "edge_postfix_increment_result",
        task: "08-16",
        review_finding: "postfix increment must preserve the old value as the expression result",
        src: "int f(void) { int i = 1; int old = i++; return old; }",
        functions: 1,
    },
    EdgeFixture {
        name: "edge_goto_out_nested_scope",
        task: "08-17",
        review_finding: "goto out of a nested scope must emit StorageDead in LIFO order",
        src: "int f(int flag) { int x = 0; { int y = 1; if (flag) goto out; x = y; } out: return x; }",
        functions: 1,
    },
    EdgeFixture {
        name: "edge_goto_around_vla_scope",
        task: "08-17",
        review_finding: "goto may jump around a VLA scope without entering the VLA lifetime",
        src: "unsigned long f(int n) { if (n < 0) goto out; { int a[n]; return sizeof a; } out: return 0; }",
        functions: 1,
    },
    EdgeFixture {
        name: "edge_sizeof_record",
        task: "08-18",
        review_finding: "sizeof(record) must use checked record layout from LayoutCx",
        src: "struct S { int a; char b; }; unsigned long f(void) { return sizeof(struct S); }",
        functions: 1,
    },
    EdgeFixture {
        name: "edge_sizeof_record_vla",
        task: "08-18",
        review_finding: "sizeof(record[n]) must multiply runtime length by record element layout",
        src: "struct S { int a; char b; }; unsigned long f(int n) { struct S a[n]; return sizeof a; }",
        functions: 1,
    },
    EdgeFixture {
        name: "edge_complex_conditional_conversion",
        task: "08-19",
        review_finding: "typeck-inserted real-to-complex conversions must survive conditional lowering",
        src: "double _Complex f(int c, double x, double _Complex z) { return c ? x : z; }",
        functions: 1,
    },
    EdgeFixture {
        name: "edge_eval_order_call_args",
        task: "08-21",
        review_finding: "call argument side effects should be stable in MIR without using runtime output as oracle",
        src: "int g(int, int); int f(void) { int a = 0; int b = 0; return g(a = 1, b = 2); }",
        functions: 1,
    },
    EdgeFixture {
        name: "edge_dead_stmt_expr_conditional_arm",
        task: "11-16f",
        review_finding: "conditional lowering must not double-terminate a dead statement-expression arm",
        src: "extern int printf(const char *, ...); void f(void) { (1 ? printf(\"ok\\n\") : ({ loop: printf(\"bad\\n\"); goto loop; })); }",
        functions: 1,
    },
    EdgeFixture {
        name: "edge_constant_false_sizeof_and",
        task: "08-29",
        review_finding: "LibTomMath-style MP_HAS false branches must not leak disabled calls into CFG",
        src: "void disabled(void); int f(int err) { if ((err != 0) && (sizeof(\"S_READ_ARC4RANDOM_C\") == 1u)) disabled(); return 0; }",
        functions: 1,
    },
];

#[test]
fn cfg_fixture_matrix_satisfies_invariants() {
    assert!(FIXTURES.len() >= 25, "task requires at least 25 CFG fixtures");
    for fixture in FIXTURES {
        let lowered = lower_snippet(fixture.name, fixture.src);
        assert_eq!(
            lowered.bodies.len(),
            fixture.functions,
            "{}: unexpected function-body count",
            fixture.name
        );
        for (def, body) in &lowered.bodies {
            verify_body_with_hir(body, &lowered.tcx, &lowered.hir)
                .unwrap_or_else(|errors| panic!("{}: verifier errors: {errors:?}", fixture.name));
            assert_body_invariants(fixture.name, *def, body);
        }
    }
}

#[test]
fn edge_source_pipeline_stabilization_fixtures() {
    assert!(
        EDGE_FIXTURES.len() >= 9,
        "expected coverage for tasks 08-16 through 08-21 plus 11-16f"
    );

    for fixture in EDGE_FIXTURES {
        let lowered = lower_snippet(fixture.name, fixture.src);
        assert_eq!(
            lowered.bodies.len(),
            fixture.functions,
            "{} ({}): unexpected function-body count; guards {}",
            fixture.name,
            fixture.task,
            fixture.review_finding,
        );
        for (def, body) in &lowered.bodies {
            verify_body_with_hir(body, &lowered.tcx, &lowered.hir).unwrap_or_else(|errors| {
                panic!(
                    "{} ({}): verifier errors for {}: {errors:?}",
                    fixture.name, fixture.task, fixture.review_finding
                )
            });
            assert_body_invariants(fixture.name, *def, body);
        }
    }
}

#[test]
fn cfg_snapshots_are_stable() {
    let cases = [
        ("if_else", "int f(int a) { int x = 0; if (a) x = 1; else x = 2; return x; }"),
        (
            "loop_break_continue",
            "int f(int n) { int i = 0; while (i < n) { i = i + 1; if (i == 3) break; } return i; }",
        ),
        (
            "switch_fallthrough",
            "int f(int x) { int y = 0; switch (x) { case 1: y = 1; case 2: y = 2; break; default: y = 3; } return y; }",
        ),
        ("call_and_pointer", "int g(int *); int f(void) { int x = 1; return g(&x); }"),
        ("vla_sizeof", "unsigned long f(int n) { int a[n]; return sizeof a; }"),
    ];

    for (name, src) in cases {
        insta::with_settings!({
            snapshot_path => "snapshots/cfg",
            prepend_module_to_snapshot => false,
            omit_expression => true,
        }, {
            insta::assert_snapshot!(name, render_snippet(name, src));
        });
    }
}

#[test]
fn edge_snapshots_are_stable() {
    let cases = [
        (
            "edge_goto_out_nested_scope",
            "int f(int flag) { int x = 0; { int y = 1; if (flag) goto out; x = y; } out: return x; }",
        ),
        (
            "edge_complex_conditional_conversion",
            "double _Complex f(int c, double x, double _Complex z) { return c ? x : z; }",
        ),
    ];

    for (name, src) in cases {
        insta::with_settings!({
            snapshot_path => "snapshots/cfg",
            prepend_module_to_snapshot => false,
            omit_expression => true,
        }, {
            insta::assert_snapshot!(name, render_snippet(name, src));
        });
    }
}

#[test]
fn eval_order_snapshots_are_stable() {
    let cases = [
        (
            "eval_order_call_args",
            "int g(int, int); int f(void) { int a = 0; int b = 0; return g(a = 1, b = 2); }",
        ),
        ("eval_order_initializer_leaves", "int f(void) { int a[3] = {1, 2, 3}; return a[0]; }"),
    ];

    for (name, src) in cases {
        insta::with_settings!({
            snapshot_path => "snapshots/cfg",
            prepend_module_to_snapshot => false,
            omit_expression => true,
        }, {
            insta::assert_snapshot!(name, render_snippet(name, src));
        });
    }
}

#[test]
fn edge_sizeof_layout_service_covers_records_and_record_vlas() {
    let record = lower_snippet(
        "edge_sizeof_record",
        "struct S { int a; char b; }; unsigned long f(void) { return sizeof(struct S); }",
    );
    let record_body = &record.bodies[0].1;
    assert!(
        body_contains_int_const(record_body, 8),
        "record sizeof should lower to checked layout size 8:\n{}",
        dump_body(record_body, &record.tcx)
    );

    let record_vla = lower_snippet(
        "edge_sizeof_record_vla",
        "struct S { int a; char b; }; unsigned long f(int n) { struct S a[n]; return sizeof a; }",
    );
    let record_vla_body = &record_vla.bodies[0].1;
    assert!(
        body_contains_len_rvalue(record_vla_body),
        "record VLA sizeof should read the saved runtime length:\n{}",
        dump_body(record_vla_body, &record_vla.tcx)
    );
    assert!(
        body_contains_int_const(record_vla_body, 8),
        "record VLA sizeof should multiply by record element size 8:\n{}",
        dump_body(record_vla_body, &record_vla.tcx)
    );
}

#[test]
fn member_access_source_pipeline_uses_resolved_field_index() {
    let lowered = lower_snippet(
        "member_field_index",
        "struct S { int a; int b; }; int f(struct S s) { return s.b; }",
    );
    let body = &lowered.bodies[0].1;
    assert!(
        body_contains_field_projection(body, 1),
        "s.b must lower to the second record field, not a fallback projection:\n{}",
        dump_body(body, &lowered.tcx)
    );
}

#[test]
fn edge_complex_conversions_are_explicit_from_source_pipeline() {
    let lowered = lower_snippet(
        "edge_complex_conditional_conversion",
        "double _Complex f(int c, double x, double _Complex z) { return c ? x : z; }",
    );
    let counts = complex_conversion_counts(&lowered.bodies[0].1);
    assert_eq!(
        counts,
        (1, 0),
        "expected one RealToComplex rvalue from the real conditional arm:\n{}",
        dump_body(&lowered.bodies[0].1, &lowered.tcx)
    );
}

#[test]
fn call_prototype_argument_conversion_reaches_cfg_operand() {
    let lowered =
        lower_snippet("call_proto_arg_conversion", "long f(long); long g(int x) { return f(x); }");
    let body = &lowered.bodies[0].1;
    let Some(arg_temp) = first_call_arg_temp(body) else {
        panic!(
            "expected prototype call to pass an argument temp:\n{}",
            dump_body(body, &lowered.tcx)
        );
    };

    assert!(
        body.blocks.iter().any(|block| {
            block.statements.iter().any(|stmt| match &stmt.kind {
                StatementKind::Assign {
                    place,
                    rvalue: Rvalue::Cast { to, kind: CastKind::IntToInt, .. },
                } => {
                    place.base == arg_temp && place.projection.is_empty() && *to == lowered.tcx.long
                }
                _ => false,
            })
        }),
        "prototype argument must be converted to long before the Call terminator:\n{}",
        dump_body(body, &lowered.tcx)
    );
}

#[test]
fn float_compound_assignment_uses_float_cast_kinds() {
    let lowered = lower_snippet(
        "float_compound_assignment",
        "float f(void) { float a = 12.34f; a += 56.78; return a; }",
    );
    let body = &lowered.bodies[0].1;
    let mut float_casts = 0usize;
    let mut bad_casts = Vec::new();

    for block in body.blocks.iter() {
        for stmt in &block.statements {
            let StatementKind::Assign { rvalue: Rvalue::Cast { op, to, kind }, .. } = &stmt.kind
            else {
                continue;
            };
            let from_ty = operand_ty(body, op);
            if is_float_ty(&lowered.tcx, from_ty) || is_float_ty(&lowered.tcx, *to) {
                if *kind == CastKind::FloatToFloat {
                    float_casts += 1;
                } else {
                    bad_casts.push((*kind, from_ty, *to));
                }
            }
        }
    }

    assert!(
        bad_casts.is_empty(),
        "float compound assignment must not use integer cast kinds: {bad_casts:?}\n{}",
        dump_body(body, &lowered.tcx)
    );
    assert!(
        float_casts >= 2,
        "expected lhs float->double and result double->float casts:\n{}",
        dump_body(body, &lowered.tcx)
    );
}

#[test]
fn common_bswap_builtin_reaches_cfg() {
    let lowered = lower_snippet(
        "builtin_bswap_cfg",
        "unsigned f(unsigned x) { return __builtin_bswap32(x); }",
    );
    let body = &lowered.bodies[0].1;
    assert!(
        body.blocks.iter().any(|block| {
            block.statements.iter().any(|stmt| {
                matches!(
                    stmt.kind,
                    StatementKind::Assign { rvalue: Rvalue::BuiltinBswap { bits: 32, .. }, .. }
                )
            })
        }),
        "runtime __builtin_bswap32 should lower to a CFG bswap rvalue:\n{}",
        dump_body(body, &lowered.tcx)
    );
}

#[test]
fn main_fallthrough_assigns_zero_to_return_slot() {
    let lowered = lower_snippet(
        "main_fallthrough_return_zero",
        "int printf(const char *, ...); int main(void) { printf(\"ok\\n\"); }",
    );
    let body = &lowered.bodies[0].1;
    assert!(
        return_slot_assigned_int_const(body, 0),
        "falling off the end of main must lower as `return 0`:\n{}",
        dump_body(body, &lowered.tcx)
    );
}

#[test]
fn builtin_unreachable_terminates_cfg_block() {
    let lowered =
        lower_snippet("builtin_unreachable_cfg", "void f(void) { __builtin_unreachable(); }");
    let body = &lowered.bodies[0].1;
    assert!(
        body.blocks
            .iter()
            .any(|block| matches!(block.terminator.kind, TerminatorKind::Unreachable)),
        "__builtin_unreachable should emit an unreachable terminator:\n{}",
        dump_body(body, &lowered.tcx)
    );
}

#[test]
fn volatile_local_qualifier_reaches_cfg_metadata() {
    let lowered = lower_snippet(
        "volatile_local_metadata",
        "int f(void) { volatile int x; int y = x; x = y; return y; }",
    );
    let body = &lowered.bodies[0].1;
    assert!(
        body.locals.iter().any(|decl| decl.quals.is_volatile),
        "volatile local qualifier must survive into CFG metadata:\n{}",
        dump_body(body, &lowered.tcx)
    );
}

#[test]
fn sizeof_layout_service_lowers_fixed_sizes() {
    let cases = [("sizeof_int_array", "unsigned long f(void) { int a[3]; return sizeof a; }", 12)];

    for (name, src, expected) in cases {
        let lowered = lower_snippet(name, src);
        let body = &lowered.bodies[0].1;
        assert!(
            body_contains_int_const(body, expected),
            "{name}: expected sizeof constant {expected} in CFG:\n{}",
            dump_body(body, &lowered.tcx)
        );
    }
}

#[test]
fn complex_conversion_rvalues_are_explicit() {
    let cases = [
        ("return_real_to_complex", "double _Complex f(double x) { return x; }", 1, 0),
        ("return_complex_to_real", "double f(double _Complex z) { return z; }", 0, 1),
        ("assignment_real_to_complex", "void f(double x) { double _Complex z; z = x; }", 1, 0),
        (
            "call_argument_real_to_complex",
            "void g(double _Complex); void f(double x) { g(x); }",
            1,
            0,
        ),
        (
            "conditional_real_to_complex_arm",
            "double _Complex f(int c, double x, double _Complex z) { return c ? x : z; }",
            1,
            0,
        ),
    ];

    for (name, src, expected_to_complex, expected_to_real) in cases {
        let lowered = lower_snippet(name, src);
        let counts = complex_conversion_counts(&lowered.bodies[0].1);
        assert_eq!(
            counts.0,
            expected_to_complex,
            "{name}: unexpected ComplexFromReal count in CFG:\n{}",
            dump_body(&lowered.bodies[0].1, &lowered.tcx)
        );
        assert_eq!(
            counts.1,
            expected_to_real,
            "{name}: unexpected RealFromComplex count in CFG:\n{}",
            dump_body(&lowered.bodies[0].1, &lowered.tcx)
        );
    }
}

#[test]
fn constant_false_logical_and_prunes_dead_call() {
    let lowered = lower_snippet(
        "constant_false_logical_and_prunes_dead_call",
        "void disabled(void); int f(int err) { if ((err != 0) && (sizeof(\"S_READ_ARC4RANDOM_C\") == 1u)) disabled(); return 0; }",
    );
    let body = &lowered.bodies[0].1;
    assert_eq!(
        call_terminator_count(body),
        0,
        "constant-false RHS of && should let if-lowering skip the disabled call:\n{}",
        dump_body(body, &lowered.tcx)
    );
}

#[test]
fn side_effectful_lhs_and_constant_false_rhs_keeps_lhs_call_only() {
    let lowered = lower_snippet(
        "side_effectful_lhs_and_constant_false_rhs_keeps_lhs_call_only",
        "int probe(void); void disabled(void); int f(void) { if (probe() && 0) disabled(); return 0; }",
    );
    let body = &lowered.bodies[0].1;
    assert_eq!(
        call_terminator_count(body),
        1,
        "f() && 0 must keep the lhs call for side effects but prune the dead body call:\n{}",
        dump_body(body, &lowered.tcx)
    );
}

#[test]
fn constant_false_if_still_lowers_goto_target_label() {
    let lowered = lower_snippet(
        "constant_false_if_still_lowers_goto_target_label",
        r#"
void *volatile sink;
int f(int limit) {
  int n = 0;
  if (0) {
  lab:;
  }
  int x[n % 1000 + 1];
  sink = x;
  n++;
  if (n < limit)
    goto lab;
  return n;
}
"#,
    );
    let body = &lowered.bodies[0].1;
    let label = body.labels.values().copied().next().expect("expected one label");
    assert!(
        !matches!(body.blocks[label].terminator.kind, TerminatorKind::Unreachable),
        "goto target label must fall through to the post-if continuation:\n{}",
        dump_body(body, &lowered.tcx)
    );
}

#[test]
fn c11_generic_selection_lowers_only_selected_runtime_arm() {
    let lowered = lower_snippet_with_options(
        "c11_generic_selection",
        r#"
        int picked(void) { return 7; }
        int unpicked(void) { return 99; }
        int f(void) {
            return _Generic(1, int: picked(), default: unpicked());
        }
        "#,
        Options { language_standard: LanguageStandard::C11, ..Options::default() },
    );
    let total_calls: usize =
        lowered.bodies.iter().map(|(_, body)| call_terminator_count(body)).sum();
    assert_eq!(
        total_calls, 1,
        "_Generic must not lower the controlling expression or unselected arms into runtime CFG"
    );
}

fn lower_snippet(name: &str, src: &str) -> Lowered {
    lower_snippet_with_options(name, src, Options::default())
}

fn lower_snippet_with_options(name: &str, src: &str, opts: Options) -> Lowered {
    let cap = CaptureEmitter::new();
    let handler = Handler::with_emitter(Box::new(cap.clone()));
    let mut session = Session::with_handler(opts, handler);
    let file = session
        .source_map
        .write()
        .unwrap()
        .add_file(PathBuf::from(format!("<cfg/{name}>")), Arc::from(src));

    let pp_tokens = rcc_preprocess::preprocess(&mut session, file);
    let ast = rcc_parse::parse(&mut session, pp_tokens).expect("parse returned None");
    let mut tcx = TyCtxt::new();
    let mut hir = lower(&ast, &mut tcx, &mut session);
    check(&mut session, &mut tcx, &mut hir);
    verify_typed_hir(&mut session, &tcx, &hir);
    assert!(
        !session.handler.has_errors(),
        "{name}: unexpected diagnostics: {:?}",
        cap.diagnostics()
    );

    let mut bodies: Vec<_> = build_bodies(&mut session, &tcx, &hir).into_iter().collect();
    bodies.sort_by_key(|(def, _)| def.0);
    Lowered { tcx, hir, bodies }
}

fn complex_conversion_counts(body: &Body) -> (usize, usize) {
    let mut to_complex = 0usize;
    let mut to_real = 0usize;
    for block in body.blocks.iter() {
        for stmt in &block.statements {
            let StatementKind::Assign { rvalue, .. } = &stmt.kind else { continue };
            match rvalue {
                Rvalue::ComplexFromReal { .. } => to_complex += 1,
                Rvalue::RealFromComplex { .. } => to_real += 1,
                _ => {}
            }
        }
    }
    (to_complex, to_real)
}

fn call_terminator_count(body: &Body) -> usize {
    body.blocks
        .iter()
        .filter(|block| matches!(block.terminator.kind, TerminatorKind::Call { .. }))
        .count()
}

fn first_call_arg_temp(body: &Body) -> Option<Local> {
    body.blocks.iter().find_map(|block| match &block.terminator.kind {
        TerminatorKind::Call { args, .. } => match args.first()? {
            Operand::Copy(place) | Operand::Move(place) if place.projection.is_empty() => {
                Some(place.base)
            }
            _ => None,
        },
        _ => None,
    })
}

fn operand_ty(body: &Body, op: &Operand) -> TyId {
    match op {
        Operand::Copy(place) | Operand::Move(place) => body.locals[place.base].ty,
        Operand::Const(c) => c.ty,
    }
}

fn is_float_ty(tcx: &TyCtxt, ty: TyId) -> bool {
    matches!(tcx.get(ty), Ty::Float(_))
}

fn body_contains_int_const(body: &Body, expected: i128) -> bool {
    body.blocks.iter().any(|block| {
        block.statements.iter().any(|stmt| match &stmt.kind {
            StatementKind::Assign { rvalue, .. } => rvalue_contains_int_const(rvalue, expected),
            _ => false,
        })
    })
}

fn return_slot_assigned_int_const(body: &Body, expected: i128) -> bool {
    body.blocks.iter().any(|block| {
        block.statements.iter().any(|stmt| {
            matches!(
                &stmt.kind,
                StatementKind::Assign {
                    place,
                    rvalue: Rvalue::Use(Operand::Const(Const {
                        kind: ConstKind::Int(value),
                        ..
                    })),
                } if place.base == Local(0) && place.projection.is_empty() && *value == expected
            )
        })
    })
}

fn body_contains_len_rvalue(body: &Body) -> bool {
    body.blocks.iter().any(|block| {
        block
            .statements
            .iter()
            .any(|stmt| matches!(stmt.kind, StatementKind::Assign { rvalue: Rvalue::Len(_), .. }))
    })
}

fn body_contains_field_projection(body: &Body, expected: u32) -> bool {
    body.blocks.iter().any(|block| {
        block.statements.iter().any(|stmt| match &stmt.kind {
            StatementKind::Assign { place, rvalue } => {
                place_contains_field_projection(place, expected)
                    || rvalue_contains_field_projection(rvalue, expected)
            }
            StatementKind::InlineAsm(asm) => {
                asm.outputs
                    .iter()
                    .any(|output| place_contains_field_projection(&output.place, expected))
                    || asm.inputs.iter().any(|input| match &input.arg {
                        InlineAsmArg::Value(operand) => {
                            operand_contains_field_projection(operand, expected)
                        }
                        InlineAsmArg::Address(place) => {
                            place_contains_field_projection(place, expected)
                        }
                    })
            }
            StatementKind::StorageLive(_) | StatementKind::StorageDead(_) | StatementKind::Nop => {
                false
            }
        }) || terminator_contains_field_projection(&block.terminator.kind, expected)
    })
}

fn terminator_contains_field_projection(term: &TerminatorKind, expected: u32) -> bool {
    match term {
        TerminatorKind::SwitchInt { discr, .. } => {
            operand_contains_field_projection(discr, expected)
        }
        TerminatorKind::Call { callee, args, destination, .. } => {
            operand_contains_field_projection(callee, expected)
                || args.iter().any(|arg| operand_contains_field_projection(arg, expected))
                || destination
                    .as_ref()
                    .is_some_and(|place| place_contains_field_projection(place, expected))
        }
        TerminatorKind::BuiltinVaStart { ap, last_param, .. } => {
            operand_contains_field_projection(ap, expected)
                || operand_contains_field_projection(last_param, expected)
        }
        TerminatorKind::BuiltinVaEnd { ap, .. } => operand_contains_field_projection(ap, expected),
        TerminatorKind::BuiltinVaCopy { dst, src, .. } => {
            operand_contains_field_projection(dst, expected)
                || operand_contains_field_projection(src, expected)
        }
        TerminatorKind::IndirectGoto { target, .. } => {
            operand_contains_field_projection(target, expected)
        }
        TerminatorKind::Goto(_) | TerminatorKind::Return | TerminatorKind::Unreachable => false,
    }
}

fn rvalue_contains_field_projection(rvalue: &Rvalue, expected: u32) -> bool {
    match rvalue {
        Rvalue::Use(op) | Rvalue::UnaryOp(_, op) | Rvalue::Cast { op, .. } => {
            operand_contains_field_projection(op, expected)
        }
        Rvalue::ComplexFromReal { real, .. } => operand_contains_field_projection(real, expected),
        Rvalue::ComplexFromParts { real, imag, .. } => {
            operand_contains_field_projection(real, expected)
                || operand_contains_field_projection(imag, expected)
        }
        Rvalue::RealFromComplex { complex, .. } => {
            operand_contains_field_projection(complex, expected)
        }
        Rvalue::BitfieldPrecision { op, .. } => operand_contains_field_projection(op, expected),
        Rvalue::VectorInit { lanes, .. } => {
            lanes.iter().any(|lane| operand_contains_field_projection(lane, expected))
        }
        Rvalue::VectorSplat { value, .. } => operand_contains_field_projection(value, expected),
        Rvalue::BinaryOp(_, lhs, rhs) => {
            operand_contains_field_projection(lhs, expected)
                || operand_contains_field_projection(rhs, expected)
        }
        Rvalue::BuiltinVaArg { ap, .. } | Rvalue::BuiltinBswap { value: ap, .. } => {
            operand_contains_field_projection(ap, expected)
        }
        Rvalue::CheckedOverflow { lhs, rhs, dst, .. } => {
            operand_contains_field_projection(lhs, expected)
                || operand_contains_field_projection(rhs, expected)
                || dst.as_ref().is_some_and(|dst| operand_contains_field_projection(dst, expected))
        }
        Rvalue::AddressOf(place) | Rvalue::Len(place) => {
            place_contains_field_projection(place, expected)
        }
        Rvalue::LoadGlobal { .. } | Rvalue::BuiltinVaArea => false,
    }
}

fn operand_contains_field_projection(operand: &Operand, expected: u32) -> bool {
    match operand {
        Operand::Copy(place) | Operand::Move(place) => {
            place_contains_field_projection(place, expected)
        }
        Operand::Const(_) => false,
    }
}

fn place_contains_field_projection(place: &Place, expected: u32) -> bool {
    place
        .projection
        .iter()
        .any(|projection| matches!(projection, Projection::Field(idx) if *idx == expected))
}

fn rvalue_contains_int_const(rvalue: &Rvalue, expected: i128) -> bool {
    match rvalue {
        Rvalue::Use(op) | Rvalue::UnaryOp(_, op) | Rvalue::Cast { op, .. } => {
            operand_contains_int_const(op, expected)
        }
        Rvalue::ComplexFromReal { real, .. } => operand_contains_int_const(real, expected),
        Rvalue::ComplexFromParts { real, imag, .. } => {
            operand_contains_int_const(real, expected) || operand_contains_int_const(imag, expected)
        }
        Rvalue::RealFromComplex { complex, .. } => operand_contains_int_const(complex, expected),
        Rvalue::BitfieldPrecision { op, .. } => operand_contains_int_const(op, expected),
        Rvalue::VectorInit { lanes, .. } => {
            lanes.iter().any(|lane| operand_contains_int_const(lane, expected))
        }
        Rvalue::VectorSplat { value, .. } => operand_contains_int_const(value, expected),
        Rvalue::BinaryOp(_, lhs, rhs) => {
            operand_contains_int_const(lhs, expected) || operand_contains_int_const(rhs, expected)
        }
        Rvalue::BuiltinVaArg { ap, .. } | Rvalue::BuiltinBswap { value: ap, .. } => {
            operand_contains_int_const(ap, expected)
        }
        Rvalue::CheckedOverflow { lhs, rhs, dst, .. } => {
            operand_contains_int_const(lhs, expected)
                || operand_contains_int_const(rhs, expected)
                || dst.as_ref().is_some_and(|dst| operand_contains_int_const(dst, expected))
        }
        Rvalue::AddressOf(_)
        | Rvalue::Len(_)
        | Rvalue::LoadGlobal { .. }
        | Rvalue::BuiltinVaArea => false,
    }
}

fn operand_contains_int_const(operand: &Operand, expected: i128) -> bool {
    matches!(
        operand,
        Operand::Const(Const { kind: ConstKind::Int(value), .. }) if *value == expected
    )
}

fn render_snippet(name: &str, src: &str) -> String {
    let lowered = lower_snippet(name, src);
    let mut out = String::new();
    for (idx, (_, body)) in lowered.bodies.iter().enumerate() {
        if idx > 0 {
            out.push('\n');
        }
        out.push_str(&dump_body(body, &lowered.tcx));
    }
    out
}

fn assert_body_invariants(name: &str, def: DefId, body: &Body) {
    assert!(!body.blocks.is_empty(), "{name}/def#{}: body has no blocks", def.0);
    assert!(!body.locals.is_empty(), "{name}/def#{}: body has no locals", def.0);
    assert_eq!(body.blocks.iter_enumerated().next().unwrap().0, BasicBlockId(0));

    if let Some(ret_ty) = body.ret_ty {
        assert_eq!(
            body.locals[Local(0)].ty,
            ret_ty,
            "{name}/def#{}: return slot type differs from body ret_ty",
            def.0
        );
    }
    assert!(
        !body.locals[Local(0)].is_param,
        "{name}/def#{}: return slot must not be a parameter",
        def.0
    );

    assert_local_order(name, def, body);
    let reachable = reachable_blocks(name, def, body);
    let mut storage_live = vec![0usize; body.locals.len()];
    let mut storage_dead = vec![0usize; body.locals.len()];

    for (bb, block) in body.blocks.iter_enumerated() {
        for stmt in &block.statements {
            match &stmt.kind {
                StatementKind::Assign { place, rvalue } => {
                    assert_place_valid(name, def, body, place);
                    assert_rvalue_valid(name, def, body, rvalue);
                }
                StatementKind::InlineAsm(asm) => {
                    for output in &asm.outputs {
                        assert_place_valid(name, def, body, &output.place);
                    }
                    for input in &asm.inputs {
                        match &input.arg {
                            InlineAsmArg::Value(operand) => {
                                assert_operand_valid(name, def, body, operand);
                            }
                            InlineAsmArg::Address(place) => {
                                assert_place_valid(name, def, body, place);
                            }
                        }
                    }
                }
                StatementKind::StorageLive(local) => {
                    assert_local_valid(name, def, body, *local);
                    storage_live[local.0 as usize] += 1;
                }
                StatementKind::StorageDead(local) => {
                    assert_local_valid(name, def, body, *local);
                    storage_dead[local.0 as usize] += 1;
                }
                StatementKind::Nop => {}
            }
        }

        assert_terminator_valid(name, def, body, bb, &block.terminator.kind);
        if reachable[bb.0 as usize] {
            assert!(
                !matches!(block.terminator.kind, TerminatorKind::Unreachable),
                "{name}/def#{}: reachable {bb:?} has default unreachable terminator",
                def.0
            );
        }
    }

    for (local, decl) in body.locals.iter_enumerated() {
        if let Some(vla_len) = decl.vla_len {
            assert_local_valid(name, def, body, vla_len);
            assert_ne!(
                local, vla_len,
                "{name}/def#{}: VLA local uses itself as length local",
                def.0
            );
        }
        if local != Local(0) && !decl.is_param && decl.name.is_some() {
            let live = storage_live[local.0 as usize];
            let dead = storage_dead[local.0 as usize];
            assert!(live > 0, "{name}/def#{}: named local {local:?} is never StorageLive", def.0);
            assert!(dead > 0, "{name}/def#{}: named local {local:?} is never StorageDead", def.0);
        }
    }
}

fn assert_local_order(name: &str, def: DefId, body: &Body) {
    let mut seen_user_local = false;
    for (local, decl) in body.locals.iter_enumerated().skip(1) {
        if decl.is_param {
            assert!(
                !seen_user_local,
                "{name}/def#{}: parameter {local:?} appears after user locals/temps",
                def.0
            );
        } else {
            seen_user_local = true;
        }
    }
}

fn reachable_blocks(name: &str, def: DefId, body: &Body) -> Vec<bool> {
    let mut reachable = vec![false; body.blocks.len()];
    let mut stack = vec![BasicBlockId(0)];
    while let Some(bb) = stack.pop() {
        assert_block_valid(name, def, body, bb);
        let idx = bb.0 as usize;
        if reachable[idx] {
            continue;
        }
        reachable[idx] = true;
        for succ in successors(&body.blocks[bb].terminator.kind) {
            assert_block_valid(name, def, body, succ);
            stack.push(succ);
        }
    }
    reachable
}

fn assert_terminator_valid(
    name: &str,
    def: DefId,
    body: &Body,
    bb: BasicBlockId,
    term: &TerminatorKind,
) {
    match term {
        TerminatorKind::Goto(target) => assert_block_valid(name, def, body, *target),
        TerminatorKind::SwitchInt { discr, targets } => {
            assert_operand_valid(name, def, body, discr);
            assert!(
                !targets.is_empty(),
                "{name}/def#{}: {bb:?} has SwitchInt without targets",
                def.0
            );
            assert!(
                targets.last().is_some_and(|(value, _)| value.is_none()),
                "{name}/def#{}: {bb:?} SwitchInt default target must be last",
                def.0
            );
            for (_, target) in targets {
                assert_block_valid(name, def, body, *target);
            }
        }
        TerminatorKind::Call { callee, args, destination, target } => {
            assert_operand_valid(name, def, body, callee);
            for arg in args {
                assert_operand_valid(name, def, body, arg);
            }
            if let Some(dest) = destination {
                assert_place_valid(name, def, body, dest);
            }
            if let Some(target) = target {
                assert_block_valid(name, def, body, *target);
            }
        }
        TerminatorKind::BuiltinVaStart { ap, last_param, target } => {
            assert_operand_valid(name, def, body, ap);
            assert_operand_valid(name, def, body, last_param);
            assert_block_valid(name, def, body, *target);
        }
        TerminatorKind::BuiltinVaEnd { ap, target } => {
            assert_operand_valid(name, def, body, ap);
            assert_block_valid(name, def, body, *target);
        }
        TerminatorKind::BuiltinVaCopy { dst, src, target } => {
            assert_operand_valid(name, def, body, dst);
            assert_operand_valid(name, def, body, src);
            assert_block_valid(name, def, body, *target);
        }
        TerminatorKind::IndirectGoto { target, targets } => {
            assert_operand_valid(name, def, body, target);
            for target in targets {
                assert_block_valid(name, def, body, *target);
            }
        }
        TerminatorKind::Return | TerminatorKind::Unreachable => {}
    }
}

fn assert_rvalue_valid(name: &str, def: DefId, body: &Body, rvalue: &Rvalue) {
    match rvalue {
        Rvalue::Use(op) | Rvalue::UnaryOp(_, op) | Rvalue::Cast { op, .. } => {
            assert_operand_valid(name, def, body, op);
        }
        Rvalue::ComplexFromReal { real, .. } => {
            assert_operand_valid(name, def, body, real);
        }
        Rvalue::ComplexFromParts { real, imag, .. } => {
            assert_operand_valid(name, def, body, real);
            assert_operand_valid(name, def, body, imag);
        }
        Rvalue::RealFromComplex { complex, .. } => {
            assert_operand_valid(name, def, body, complex);
        }
        Rvalue::BitfieldPrecision { op, .. } => {
            assert_operand_valid(name, def, body, op);
        }
        Rvalue::VectorInit { lanes, .. } => {
            for lane in lanes {
                assert_operand_valid(name, def, body, lane);
            }
        }
        Rvalue::VectorSplat { value, .. } => {
            assert_operand_valid(name, def, body, value);
        }
        Rvalue::BinaryOp(_, lhs, rhs) => {
            assert_operand_valid(name, def, body, lhs);
            assert_operand_valid(name, def, body, rhs);
        }
        Rvalue::BuiltinVaArg { ap, .. } => {
            assert_operand_valid(name, def, body, ap);
        }
        Rvalue::BuiltinBswap { value, .. } => {
            assert_operand_valid(name, def, body, value);
        }
        Rvalue::CheckedOverflow { lhs, rhs, dst, .. } => {
            assert_operand_valid(name, def, body, lhs);
            assert_operand_valid(name, def, body, rhs);
            if let Some(dst) = dst {
                assert_operand_valid(name, def, body, dst);
            }
        }
        Rvalue::AddressOf(place) | Rvalue::Len(place) => {
            assert_place_valid(name, def, body, place);
        }
        Rvalue::LoadGlobal { .. } | Rvalue::BuiltinVaArea => {}
    }
}

fn assert_operand_valid(name: &str, def: DefId, body: &Body, operand: &Operand) {
    match operand {
        Operand::Copy(place) | Operand::Move(place) => assert_place_valid(name, def, body, place),
        Operand::Const(Const { .. }) => {}
    }
}

fn assert_place_valid(name: &str, def: DefId, body: &Body, place: &Place) {
    if !matches!(place.projection.first(), Some(Projection::Global(_))) {
        assert_local_valid(name, def, body, place.base);
    }
    for projection in &place.projection {
        match projection {
            Projection::Global(_) => {}
            Projection::Deref | Projection::Field(_) => {}
            Projection::Index(index) => assert_operand_valid(name, def, body, index),
        }
    }
}

fn assert_local_valid(name: &str, def: DefId, body: &Body, local: Local) {
    assert!(
        (local.0 as usize) < body.locals.len(),
        "{name}/def#{}: local {local:?} out of range 0..{}",
        def.0,
        body.locals.len()
    );
}

fn assert_block_valid(name: &str, def: DefId, body: &Body, bb: BasicBlockId) {
    assert!(
        (bb.0 as usize) < body.blocks.len(),
        "{name}/def#{}: block {bb:?} out of range 0..{}",
        def.0,
        body.blocks.len()
    );
}

fn successors(term: &TerminatorKind) -> Vec<BasicBlockId> {
    match term {
        TerminatorKind::Goto(target) => vec![*target],
        TerminatorKind::SwitchInt { targets, .. } => {
            targets.iter().map(|(_, target)| *target).collect()
        }
        TerminatorKind::Call { target: Some(target), .. } => vec![*target],
        TerminatorKind::Call { target: None, .. }
        | TerminatorKind::Return
        | TerminatorKind::Unreachable => Vec::new(),
        TerminatorKind::BuiltinVaStart { target, .. }
        | TerminatorKind::BuiltinVaEnd { target, .. }
        | TerminatorKind::BuiltinVaCopy { target, .. } => vec![*target],
        TerminatorKind::IndirectGoto { targets, .. } => targets.clone(),
    }
}
