name = "Vec Test"
id = hero_id.any

local pass_count = 0
local fail_count = 0

local function check(label, got, expected)
    local ok = false
    if type(expected) == "number" then
        ok = math.abs(got - expected) < 0.001
    elseif type(expected) == "string" then
        ok = tostring(got) == expected
    elseif type(expected) == "boolean" then
        ok = got == expected
    else
        ok = got == expected
    end

    if ok then
        pass_count = pass_count + 1
    else
        fail_count = fail_count + 1
        print("FAIL: " .. label .. " | got: " .. tostring(got) .. " expected: " .. tostring(expected))
    end
end

function is_enabled()
    return input.is_key_held(VK.F6)
end

function on_tick()
    pass_count = 0
    fail_count = 0

    -- ========== vec3 ==========
    print("--- vec3 ---")

    -- constructors
    local a = vec3(1.0, 2.0, 3.0)
    local b = vec3(4.0, 5.0, 6.0)
    local zero = vec3()
    check("vec3() default", tostring(zero), "vec3(0.000000, 0.000000, 0.000000)")

    -- .x .y .z read (must be number, not function)
    check("a.x type", type(a.x), "number")
    check("a.y type", type(a.y), "number")
    check("a.z type", type(a.z), "number")
    check("a.x value", a.x, 1.0)
    check("a.y value", a.y, 2.0)
    check("a.z value", a.z, 3.0)

    -- .x .y .z write
    local c = vec3(0.0, 0.0, 0.0)
    c.x = 10.0
    c.y = 20.0
    c.z = 30.0
    check("write c.x", c.x, 10.0)
    check("write c.y", c.y, 20.0)
    check("write c.z", c.z, 30.0)

    -- arithmetic: vec3 + vec3
    local sum = a + b
    check("a+b .x", sum.x, 5.0)
    check("a+b .y", sum.y, 7.0)
    check("a+b .z", sum.z, 9.0)

    -- arithmetic: vec3 - vec3
    local diff = a - b
    check("a-b .x", diff.x, -3.0)
    check("a-b .y", diff.y, -3.0)
    check("a-b .z", diff.z, -3.0)

    -- arithmetic: vec3 * scalar
    local scaled = a * 2.0
    check("a*2 .x", scaled.x, 2.0)
    check("a*2 .y", scaled.y, 4.0)
    check("a*2 .z", scaled.z, 6.0)

    -- arithmetic: vec3 / scalar
    local halved = b / 2.0
    check("b/2 .x", halved.x, 2.0)
    check("b/2 .y", halved.y, 2.5)
    check("b/2 .z", halved.z, 3.0)

    -- arithmetic: vec3 * vec3
    local mul = a * b
    check("a*b .x", mul.x, 4.0)
    check("a*b .y", mul.y, 10.0)
    check("a*b .z", mul.z, 18.0)

    -- equality
    check("a == a", a == a, true)
    check("a == b", a == b, false)
    check("vec3(1,2,3) == vec3(1,2,3)", vec3(1.0, 2.0, 3.0) == a, true)

    -- empty
    check("zero:empty()", zero:empty(), true)
    check("a:empty()", a:empty(), false)

    -- length: sqrt(1+4+9) = sqrt(14) ~ 3.7416
    check("a:length()", a:length(), 3.7416)

    -- length_sqr: 1+4+9 = 14
    check("a:length_sqr()", a:length_sqr(), 14.0)

    -- length_2d: sqrt(1+4) = sqrt(5) ~ 2.2360
    check("a:length_2d()", a:length_2d(), 2.2360)

    -- distance: same as (a-b):length() = sqrt(27) ~ 5.1961
    check("a:distance(b)", a:distance(b), 5.1961)

    -- dot product: 1*4 + 2*5 + 3*6 = 32
    check("a:dot(b)", a:dot(b), 32.0)

    -- cross product: (2*6-3*5, 3*4-1*6, 1*5-2*4) = (-3, 6, -3)
    local cr = a:cross(b)
    check("cross .x", cr.x, -3.0)
    check("cross .y", cr.y, 6.0)
    check("cross .z", cr.z, -3.0)

    -- normalized: a / |a|
    local n = a:normalized()
    check("normalized length", n:length(), 1.0)
    check("normalized .x", n.x, 1.0 / 3.7416)

    -- angle_to: angle between a and b
    local angle = a:angle_to(b)
    check("angle_to > 0", angle > 0.0, true)
    check("angle_to < 90", angle < 90.0, true)
    -- same vector ~ 0 degrees (fp precision)
    check("angle_to self", a:angle_to(a) < 0.1, true)

    -- tostring
    check("tostring prefix", tostring(a):sub(1, 4), "vec3")

    -- gravity prediction (per-component math)
    local gpos = vec3(100.0, 200.0, 500.0)
    local gvel = vec3(10.0, -5.0, 2.0)
    local tof = 0.5
    local gravity = 800.0
    local predicted = vec3(
        gpos.x + gvel.x * tof,
        gpos.y + gvel.y * tof,
        gpos.z + gvel.z * tof - 0.5 * gravity * tof * tof
    )
    check("gravity pred .x", predicted.x, 105.0)
    check("gravity pred .y", predicted.y, 197.5)
    check("gravity pred .z", predicted.z, 401.0)

    -- ========== vec2 ==========
    print("--- vec2 ---")

    local d = vec2(3.0, 4.0)
    local e = vec2(1.0, 2.0)
    local zero2 = vec2()

    -- constructors
    check("vec2() default", tostring(zero2), "vec2(0.000000, 0.000000)")

    -- .x .y read
    check("d.x type", type(d.x), "number")
    check("d.y type", type(d.y), "number")
    check("d.x value", d.x, 3.0)
    check("d.y value", d.y, 4.0)

    -- .x .y write
    local f = vec2(0.0, 0.0)
    f.x = 7.0
    f.y = 8.0
    check("write f.x", f.x, 7.0)
    check("write f.y", f.y, 8.0)

    -- arithmetic
    local sum2 = d + e
    check("d+e .x", sum2.x, 4.0)
    check("d+e .y", sum2.y, 6.0)

    local diff2 = d - e
    check("d-e .x", diff2.x, 2.0)
    check("d-e .y", diff2.y, 2.0)

    local scaled2 = d * 3.0
    check("d*3 .x", scaled2.x, 9.0)
    check("d*3 .y", scaled2.y, 12.0)

    local halved2 = d / 2.0
    check("d/2 .x", halved2.x, 1.5)
    check("d/2 .y", halved2.y, 2.0)

    local mul2 = d * e
    check("d*e .x", mul2.x, 3.0)
    check("d*e .y", mul2.y, 8.0)

    -- equality
    check("d == d", d == d, true)
    check("d == e", d == e, false)

    -- empty
    check("zero2:empty()", zero2:empty(), true)
    check("d:empty()", d:empty(), false)

    -- length: sqrt(9+16) = 5
    check("d:length()", d:length(), 5.0)

    -- length_sqr: 9+16 = 25
    check("d:length_sqr()", d:length_sqr(), 25.0)

    -- distance: sqrt(4+4) ~ 2.8284
    check("d:distance(e)", d:distance(e), 2.8284)

    -- dot: 3*1 + 4*2 = 11
    check("d:dot(e)", d:dot(e), 11.0)

    -- normalized: d / 5
    local n2 = d:normalized()
    check("normalized2 length", n2:length(), 1.0)
    check("normalized2 .x", n2.x, 0.6)
    check("normalized2 .y", n2.y, 0.8)

    -- angle_to
    local angle2 = d:angle_to(e)
    check("angle_to type", type(angle2), "number")

    -- tostring
    check("tostring2 prefix", tostring(d):sub(1, 4), "vec2")

    -- per-axis noise
    local base = vec2(100.0, 200.0)
    local noisy = vec2(base.x + 1.5, base.y - 0.8)
    check("noise .x", noisy.x, 101.5)
    check("noise .y", noisy.y, 199.2)

    -- ========== real world ==========
    print("--- real world ---")
    local lp = local_player()
    if lp then
        local lp_pos = lp:get_position()
        check("lp pos.x type", type(lp_pos.x), "number")
        check("lp pos.y type", type(lp_pos.y), "number")
        check("lp pos.z type", type(lp_pos.z), "number")
        check("lp pos not zero", lp_pos:empty(), false)

        -- roundtrip: extract -> reconstruct -> compare
        local rebuilt = vec3(lp_pos.x, lp_pos.y, lp_pos.z)
        check("roundtrip distance", lp_pos:distance(rebuilt), 0.0)

        -- z comparison
        if lp_pos.z > 0.0 then
            print("player z = " .. tostring(lp_pos.z))
        end
    else
        print("no local player, skipping real world tests")
    end

    -- ========== results ==========
    print("")
    print("PASSED: " .. tostring(pass_count) .. " | FAILED: " .. tostring(fail_count))
    if fail_count == 0 then
        print("ALL TESTS PASSED")
    end

    sleep(3000)
end

settings = {}
