//! End-to-end regression tests for verified miscompiles and strict-WGSL
//! emission bugs.  Each runs the full `parse -> IR passes -> generate`
//! pipeline via [`crate::run`] at the default (`max`) profile and asserts a
//! semantic invariant on the minified text; a failure means a previously
//! reproduced defect - valid-but-wrong output, or a form strict WGSL parsers
//! reject - is back.

use super::helpers::assert_valid_wgsl;
use crate::config::Config;

/// Run the full pipeline at the default profile and return minified text.
fn minify(src: &str) -> String {
    let out = crate::run(src, &Config::default()).expect("run failed");
    assert_valid_wgsl(&out.source);
    out.source
}

/// An identity fold (`0 + x`) over a single-use storage `Load` must NOT
/// relocate the read past an intervening `Store` to the same place; doing so
/// would make `data[1] = 0u + data[0]` read the post-`Store` value.
#[test]
fn identity_fold_does_not_reorder_load_past_store() {
    let src = "@group(0)@binding(0) var<storage,read_write> data:array<u32>;\
        @compute @workgroup_size(1) fn main(){ let a=data[0]; data[0]=99u; data[1]=0u+a; }";
    let out = minify(src);
    // The load is bound BEFORE the store; the second store uses the bound
    // value, never a fresh `A[0]` read after the overwrite.
    assert!(
        out.contains("let a=A[0];A[0]=99;A[1]=0+a;"),
        "identity fold reordered a load past a store: {out}"
    );
    assert!(
        !out.contains("A[1]=A[0]"),
        "data[1] must not re-read A[0] after it is overwritten: {out}"
    );
}

/// The involution fold (`-(-x)`) shares the load-relocation hazard, and the
/// surviving double-negation must emit `- -a` - a space disambiguates the
/// WGSL-reserved `--` token.
#[test]
fn involution_fold_does_not_reorder_load_and_avoids_double_minus() {
    let src = "@group(0)@binding(0) var<storage,read_write> data:array<i32>;\
        @compute @workgroup_size(1) fn main(){ let a=data[0]; data[0]=99; data[1]=-(-a); }";
    let out = minify(src);
    assert!(
        out.contains("let a=A[0];A[0]=99;A[1]=- -a;"),
        "involution reordered a load past a store or emitted `--`: {out}"
    );
    assert!(
        !out.contains("--"),
        "must not emit the WGSL-reserved `--` token: {out}"
    );
}

/// A single-use `textureLoad` must be bound to a `let` before an intervening
/// `textureStore` to the same storage texture, not inlined past it (it would
/// read the post-store texel).
#[test]
fn image_load_bound_before_intervening_texture_store() {
    let src = "@group(0)@binding(0) var tex: texture_storage_2d<r32float, read_write>;\
        @compute @workgroup_size(1) fn main(){\
          let c = vec2i(0,0);\
          let v = textureLoad(tex, c).x;\
          textureStore(tex, c, vec4f(99.0));\
          textureStore(tex, vec2i(1,1), vec4f(v));\
        }";
    let out = minify(src);
    assert!(
        out.contains("let B=textureLoad(A,a);textureStore(A,a,"),
        "textureLoad must be bound before the intervening textureStore: {out}"
    );
}

/// Loop CSE must not redirect a `continuing`-block expression to a body `let`
/// defined after a `continue` (the continue iteration skips it); the
/// continuing block must recompute the shared expression.
#[test]
fn cse_does_not_share_body_let_into_continuing_past_continue() {
    let src = "@group(0) @binding(0) var<storage, read_write> buf: array<f32, 64>;\
        @group(0) @binding(1) var<uniform> k: f32;\
        @compute @workgroup_size(1)\
        fn main(@builtin(global_invocation_id) gid: vec3<u32>) {\
            let p = f32(gid.x);\
            var i = 0u; var acc = 0.0;\
            loop {\
                if i >= 10u { break; }\
                let w = acc + p;\
                if i == 5u { continue; }\
                let a = sin(w)*cos(k) + tan(w)*sin(k);\
                buf[i] = a; buf[i + 1u] = a;\
                continuing {\
                    let b = sin(w)*cos(k) + tan(w)*sin(k);\
                    buf[0] = b; acc = acc + 1.0; i = i + 1u;\
                }\
            }\
        }";
    let out = minify(src);
    // The continuing-block store recomputes its own `sin(...)` chain rather
    // than referencing a body binding the `continue` skips.  Name-agnostic and
    // emission-shape-agnostic: a recompute leaves a `sin(` inside the
    // continuing block (whether inlined into the store or rebound), whereas a
    // buggy reuse would reference the body binding and emit no `sin(` there.
    let continuing = out
        .split_once("continuing{")
        .map(|(_, rest)| rest)
        .expect("a continuing block must be emitted");
    assert!(
        continuing.contains("sin("),
        "continuing must recompute the shared expression, not reuse a body let: {out}"
    );
}

/// Redundant-store elimination must compare float literals bitwise, so a
/// conditional `+0.0` store against a known `-0.0` init is NOT dropped - the
/// sign of zero is observable.
#[test]
fn neg_zero_conditional_store_is_not_dropped() {
    let src = "@fragment fn main(@location(0) c: f32, @location(1) d: f32) -> @location(0) vec4f {\
        var x: f32 = -0.0; if c > 0.5 { x = 0.0; } let r = x + d; return vec4f(r); }";
    let out = minify(src);
    // The `if` and its `x = 0.0` store survive; `x` is not hardcoded to -0.0.
    // Name-agnostic: the `-0f` init keeps its sign (not folded to +0.0), and
    // both the guard (`>.5{`) and the conditional `=0` store survive.
    assert!(
        out.contains("=-0f"),
        "the -0.0 initializer must keep its sign: {out}"
    );
    assert!(
        out.contains(">.5{") && out.contains("=0;}"),
        "the +0.0-vs--0.0 conditional store was wrongly eliminated: {out}"
    );
}

/// Swizzle/member access on a `ptr<function, vecN>` value must emit an
/// explicit `(*p)` deref - strict WGSL parsers reject `p.xyz`.
#[test]
fn pointer_value_swizzle_is_dereferenced() {
    let src = "fn rd(q: ptr<function, vec4<f32>>) -> vec3<f32> {\
          return vec3<f32>((*q).x, (*q).y, (*q).z) * 2.0 + vec3<f32>((*q).z, (*q).y, (*q).x);\
        }\
        @fragment fn main(@location(0) k: f32) -> @location(0) vec4f {\
          var v = vec4f(k, k*2.0, k*3.0, k*4.0);\
          var w = vec4f(k*5.0, k*6.0, k*7.0, k*8.0);\
          let r = rd(&v) + rd(&w);\
          return vec4f(r, 1.0);\
        }";
    let out = minify(src);
    assert!(
        out.contains("(*a).xyz"),
        "pointer-value swizzle must be dereferenced as (*p).xyz: {out}"
    );
    // The bug form is a bare swizzle on the pointer parameter name.
    assert!(
        !out.contains("return a.xyz") && !out.contains("a)->vec3f{return a."),
        "must not emit a bare swizzle on a pointer value: {out}"
    );
}

/// An override WITHOUT an explicit `@id` is keyed by its name in the host
/// pipeline `constants` record, so mangling must preserve it; an `@id`-bearing
/// override is keyed numerically and stays renamable.
#[test]
fn id_less_override_name_is_preserved_under_mangle() {
    let src = "@id(7) override has_id: f32 = 1.0;\
        override no_id: f32 = 2.0;\
        @fragment fn main() -> @location(0) vec4f { return vec4f(has_id * no_id); }";
    let out = minify(src);
    assert!(
        out.contains("override no_id:"),
        "@id-less override name must be preserved: {out}"
    );
    assert!(
        !out.contains("override has_id:"),
        "@id-bearing override should still be mangled: {out}"
    );
}

/// A vector `Compose` built from sub-vectors (`vec4(v2, v2)`) must not be
/// splat-elided to a non-scalar operand; doing so emits `v2 * v4` and forces a
/// fallback to naga's verbose emitter.
#[test]
fn subvector_compose_not_splat_elided() {
    let src = "@group(0)@binding(0) var<uniform> v: vec2f;\
        @group(0)@binding(1) var<uniform> w: vec4f;\
        @fragment fn main() -> @location(0) vec4f { let a = v; return vec4<f32>(a, a) * w; }";
    let out = minify(src);
    // The compose stays a full vec4 constructor; no bare `vec2 * vec4`.
    assert!(
        out.contains("vec4f(A,A)*a") || out.contains("vec4f(A,A) * a"),
        "sub-vector compose was wrongly splat-elided: {out}"
    );
}

/// A storage texture reached through a `binding_array` (`texs[i]`) is still a
/// hazardous-load target: a single-use `textureLoad(texs[i], ..)` must be
/// bound before an intervening `textureStore` to the same element, not inlined
/// past it.  The writability test must peel the `BindingArray` to the element
/// texture type, else the load is dropped from the hazard set and reordered.
#[test]
fn binding_array_texture_load_bound_before_store() {
    let src = "@group(0) @binding(0) var texs: binding_array<texture_storage_2d<r32uint, read_write>, 4>;\
        @compute @workgroup_size(1) fn main(){\
          let c = vec2i(0,0);\
          let v = textureLoad(texs[0], c).x;\
          textureStore(texs[0], c, vec4u(99u));\
          textureStore(texs[0], vec2i(1,1), vec4u(v));\
        }";
    let out = minify(src);
    assert!(
        out.contains("let B=textureLoad(A[0],a);textureStore(A[0],a,"),
        "binding-array textureLoad must be bound before the intervening store: {out}"
    );
}

/// An inlined whole-pointee `Load` of a pointer value (`let e = (*p)`) used as
/// a postfix base must be parenthesised - `(*p).field`, not the bare
/// `*p.field`, which parses as `*(p.field)` and is rejected (forcing a verbose
/// naga fallback).
#[test]
fn inlined_pointee_load_postfix_base_is_parenthesized() {
    let src = "struct M { mx: vec4<f32>, my: vec4<f32> }\
        fn f(d: ptr<function, M>, s: f32) { let e = (*d); (*d).mx = e.mx + vec4f(s); }\
        @fragment fn main() -> @location(0) vec4f { var m: M; m.my = vec4f(1.0); f(&m, 2.0); return m.mx; }";
    let out = minify(src);
    // Names are mangled, so assert the structural invariant rather than exact
    // ids: the inlined deref is parenthesised as `(*ptr).member`, never the
    // WGSL-invalid `*ptr.member` (which parses as `*(ptr.member)`).  The
    // deref'd pointer is the function's first argument, `a`.
    assert!(
        out.contains("(*a)."),
        "inlined pointee-Load postfix base must be parenthesised `(*p).field`: {out}"
    );
    assert!(
        !out.contains("*a."),
        "must not emit the WGSL-invalid unparenthesised `*ptr.field`: {out}"
    );
    // It must NOT have fallen back to naga's verbose multi-line emitter (which
    // the pre-fix invalid `*a.mx` forced).
    assert!(
        out.matches('\n').count() <= 1,
        "output looks like the verbose naga fallback (lost minification): {out}"
    );
}

/// A `var<immediate>` (push-constant / immediate-data) global must keep its
/// address space.  Emitting `var<private>` instead (a per-invocation,
/// zero-initialised mutable with a different data source) is a silent
/// miscompile: the shader reads default-initialised memory rather than the
/// host-supplied immediate data.  Guards against `address_space` mapping
/// `AddressSpace::Immediate` to the `"private"` fallback.
#[test]
fn immediate_address_space_is_preserved() {
    let src = "var<immediate> pc: vec4<f32>;\
        @fragment fn main() -> @location(0) vec4<f32> { return pc; }";
    let out = minify(src);
    assert!(
        out.contains("var<immediate>"),
        "var<immediate> must be preserved, not rewritten to another space: {out}"
    );
    assert!(
        !out.contains("var<private>"),
        "var<immediate> must not become var<private> (changes the data source): {out}"
    );
}

/// An f16 value that survives only as a bare literal inside a conversion
/// (`f32(1.0h + 2.0h)` folds to `f32(3h)`) still requires `enable f16;` in
/// the output.  The directive scan must catch f16 *literals* / casts, not
/// just registered f16 *types*; otherwise the emitted text is rejected by
/// naga ("the `f16` enable extension is not enabled") and the whole run
/// fails.  `minify` re-parses + validates, so a missing directive panics here.
#[test]
fn enable_f16_emitted_for_surviving_f16_literal_in_conversion() {
    // No explicit `enable f16;`: detection must inject it on input AND the
    // emitter must re-assert it on output for the folded `f32(3h)`.
    let src = "@fragment fn main() -> @location(0) vec4<f32> { \
        let x = 1.0h + 2.0h; return vec4<f32>(f32(x), 0.0, 0.0, 1.0); }";
    let out = minify(src);
    assert!(
        out.contains("enable f16;"),
        "output uses an f16 literal but dropped `enable f16;`: {out}"
    );
}

/// `lhs = rhs * lhs` must NOT fold to the compound `lhs *= rhs` when the
/// product is a non-commutative linear-algebra product (matrix*matrix,
/// matrix*vector, vector*matrix): `*=` desugars to `lhs = lhs * rhs`, the
/// opposite (transposed) product.  A matrix `m = n * m` must stay `m = n * m`.
#[test]
fn matrix_compound_assign_preserves_non_commutative_order() {
    // mat*mat, rhs-self: the swapped fold would be a silent miscompile.
    let mm = minify(
        "@group(0)@binding(0) var<storage,read_write> m: mat3x3<f32>;\
         @group(0)@binding(1) var<storage,read> n: mat3x3<f32>;\
         @compute @workgroup_size(1) fn main() { m = n * m; }",
    );
    assert!(
        mm.contains("A=a*A;") && !mm.contains("*="),
        "mat*mat `m = n*m` was folded to a swapped `*=` (transposed product): {mm}"
    );
    // mat*vec, rhs-self: `v = M * v` must keep the column-vector product.
    let mv = minify(
        "@group(0)@binding(0) var<storage,read_write> v: vec3<f32>;\
         @group(0)@binding(1) var<storage,read> M: mat3x3<f32>;\
         @compute @workgroup_size(1) fn main() { v = M * v; }",
    );
    assert!(
        mv.contains("A=a*A;") && !mv.contains("*="),
        "mat*vec `v = M*v` was folded to a swapped `*=`: {mv}"
    );
    // The left-self matrix fold IS order-preserving and must still apply.
    let ls = minify(
        "@group(0)@binding(0) var<storage,read_write> m: mat3x3<f32>;\
         @group(0)@binding(1) var<storage,read> n: mat3x3<f32>;\
         @compute @workgroup_size(1) fn main() { m = m * n; }",
    );
    assert!(
        ls.contains("A*=a;"),
        "left-self matrix `m = m*n` should still fold to `m *= n`: {ls}"
    );
    // A genuinely commutative matrix*scalar must still fold (no over-restrict).
    let ms = minify(
        "@group(0)@binding(0) var<storage,read_write> m: mat3x3<f32>;\
         @group(0)@binding(1) var<storage,read> k: f32;\
         @compute @workgroup_size(1) fn main() { m = k * m; }",
    );
    assert!(
        ms.contains("A*=a;"),
        "commutative matrix*scalar `m = k*m` should still fold to `m *= k`: {ms}"
    );
}

/// A both-literal F16 multiply by zero (`x * 0.0h`) must NOT be folded by the
/// absorbing rule, because `eval_binary` has no F16 arm to compute the IEEE
/// sign of the product: cloning the matched `0.0h` would take its sign, not
/// the product's (`-2.0h * 0.0h` is `-0.0h`, not `+0.0h`).  The fold is
/// declined so the bare product survives and re-parses to the right signed
/// zero.  Guards the `is_integer_zero` gate on the absorbing-Multiply arm.
#[test]
fn f16_multiply_by_zero_is_not_mis_signed() {
    let out = minify(
        "enable f16;\
         @group(0)@binding(0) var<storage,read_write> out: array<f16, 4>;\
         @compute @workgroup_size(1) fn main() { let a = -2.0h; out[0] = a * 0.0h; }",
    );
    // The signed product must survive as `-2h*0h` (re-parses to -0.0h), never
    // collapse to a positive-zero store `A[0]=0h;`.
    assert!(
        out.contains("-2h*0h"),
        "f16 `-2.0h * 0.0h` must keep the signed product, not fold to +0.0h: {out}"
    );
    assert!(
        !out.contains("A[0]=0h"),
        "f16 `-2.0h * 0.0h` was mis-signed to +0.0h: {out}"
    );
}

/// An override-sized array (`array<i32, O*2>`) must round-trip without
/// aborting the process.  naga 29's own WGSL back-end hits `unreachable!()`
/// on the override size expression; under the release `panic = "abort"`
/// strategy that crashes (SIGABRT).  nagami emits these types with its own
/// generator, so `run` must detect the override-sized array and skip the
/// naga baseline/fallback emit rather than invoke it.
#[test]
fn override_sized_array_does_not_abort() {
    let out = minify(
        "override O = 123;\
         alias A = array<i32, O*2>;\
         var<workgroup> W: A;\
         @compute @workgroup_size(1) fn main() { let p: ptr<workgroup, A> = &W; (*p)[0] = 42; }",
    );
    // `minify` re-parses + validates, so reaching here means no abort and a
    // valid override-sized array survived.
    assert!(
        out.contains("array<i32,") && out.contains("var<workgroup>"),
        "override-sized workgroup array must survive minification: {out}"
    );
}
