test_result(a::AbstractArray, b::AbstractArray; kwargs...) =
    isapprox(Array(a), Array(b); kwargs...)
test_result(a::Number, b::Number; kwargs...) = isapprox(a, b; kwargs...)

function compare(f, AT::Type, xs...; kwargs...)
    cpu_in = map(deepcopy, xs) # copy on CPU
    gpu_in = map(adapt(AT), xs) # adapt on GPU

    cpu_out = f(cpu_in...)
    gpu_out = f(gpu_in...)
    return test_result(cpu_out, gpu_out; kwargs...)
end

# types to test for
ATs = []
!is_buildkite && push!(ATs, JLArray)
CUDACore.functional() && cuBLAS.functional() && push!(ATs, CuArray)
AMDGPU.functional() && push!(ATs, ROCArray)
Metal.functional() && push!(ATs, MtlArray)

@testset "isblasmatrix ($AT)" for AT in ATs
    for T in (Float32, ComplexF32)
        A1 = StridedView(AT(randn(T, 20, 20)))
        @test Strided.isblasmatrix(A1)
        A2 = view(A1, 1:4:20, 1:5:20)
        @test !Strided.isblasmatrix(A2)
        A3 = view(conj!(A1), 1:4:20, 1:20) # stride(A3, 2) is not 1
        @test !Strided.isblasmatrix(A3)
    end
end

@testset "in-place matrix operations ($AT)" for AT in ATs
    for T in (Float32, ComplexF32)
        A1 = StridedView(randn(T, 20, 20))
        A2 = StridedView(randn(T, 20, 20))

        @test compare(conj!, AT, A1)
        @test compare(adjoint!, AT, A1, A2)
        @test compare(transpose!, AT, A1, A2)
        @test compare((x, y) -> permutedims!(x, y, (2, 1)), AT, A1, A2)

        B1 = A1[4:4:end, 1:4:end]
        B2 = A2[4:4:end, 1:4:end]

        @test compare(conj!, AT, B1)
        @test compare(adjoint!, AT, B1, B2)
        @test compare(transpose!, AT, B1, B2)
        @test compare((x, y) -> permutedims!(x, y, (2, 1)), AT, B1, B2)
    end
end

@testset "mul! ($AT{$T})" for AT in ATs, T in (Float32, ComplexF32)
    N = 2
    α = rand(T)
    β = rand(T)
    dims = ntuple(Returns(div(64, N)), N)
    A1 = permutedims(StridedView(rand(T, dims)), randperm(N))
    A2 = permutedims(StridedView(rand(T, dims)), randperm(N))
    A3 = permutedims(StridedView(rand(T, dims)), randperm(N))
    @test compare((C, A, B) -> mul!(C, A, B, α, β), AT, A1, A2, A3)
    # test BLAS for all op combinations
    @testset for sz in ((32, 64), (64, 64), (64, 32))
        vA1 = view(StridedView(rand(T, sz)), 1:32, 1:32)
        vA2 = view(StridedView(rand(T, sz)), 1:32, 1:32)
        vA3 = view(StridedView(rand(T, sz)), 1:32, 1:32)
        @testset for f1 in (identity, conj, adjoint, transpose), f2 in (identity, conj, adjoint, transpose)
            @test compare((C, A, B) -> mul!(C, A, B, α, β), AT, vA1, f1(vA2), f2(vA3))
        end
    end
    # non-BLAS fallback path
    vA1 = view(StridedView(rand(T, (32, 32))), 1:32, 1:32)
    vA2 = view(StridedView(rand(T, (32, 64))), 1:32, 1:2:64)
    vA3 = view(StridedView(rand(T, (64, 32))), 1:2:64, 1:32)
    @testset for f1 in (identity, conj, adjoint, transpose), f2 in (identity, conj, adjoint, transpose)
        @test compare((C, A, B) -> mul!(C, A, B, α, β), AT, vA1, f1(vA2), f2(vA3))
    end
    # non-BLAS fallback path
    vA1 = view(StridedView(rand(T, (64, 32))), 1:2:64, 1:32)
    vA2 = view(StridedView(rand(T, (32, 64))), 1:32, 1:2:64)
    vA3 = view(StridedView(rand(T, (64, 32))), 1:2:64, 1:32)
    @test compare((C, A, B) -> mul!(C, A, B, α, β), AT, vA1, vA2, vA3)
end

@testset "map, scale!, axpy!, axpby! ($AT)" for AT in ATs
    for T in (Float32, ComplexF32)
        for N in 2:6
            dims = ntuple(Returns(div(60, N)), N)
            A1 = permutedims(StridedView(rand(T, dims)), randperm(N))
            A2 = permutedims(StridedView(rand(T, dims)), randperm(N))
            A3 = permutedims(StridedView(rand(T, dims)), randperm(N))

            @test compare(x -> rmul!(x, 1 // 2), AT, A1)
            @test compare(x -> lmul!(1 // 3, x), AT, A2)
            @test compare((x, y) -> axpy!(1 // 3, x, y), AT, A1, A2)
            @test compare((x, y) -> axpby!(1 // 3, x, 1 // 2, y), AT, A1, A2)
            @test compare((x, y, z) -> map((a, b, c) -> sin(a) + b / exp(-abs(c)), x, y, z), AT, A1, A2, A3)
            @test compare((x, y) -> mul!(x, 1, y), AT, A1, A2)
            @test compare((x, y) -> mul!(x, y, 1), AT, A1, A2)
        end

        dims = ntuple(Returns(20), 2)
        A1 = permutedims(StridedView(rand(T, dims))[2:2:end, 2:2:end], randperm(2))
        A2 = permutedims(StridedView(rand(T, dims))[2:2:end, 2:2:end], randperm(2))
        A3 = permutedims(StridedView(rand(T, dims))[2:2:end, 2:2:end], randperm(2))
        @test compare(x -> rmul!(x, 1 // 2), AT, A1)
        @test compare(x -> lmul!(1 // 3, x), AT, A2)
        @test compare((x, y) -> axpy!(1 // 3, x, y), AT, A1, A2)
        @test compare((x, y) -> axpby!(1 // 3, x, 1 // 2, y), AT, A1, A2)
        @test compare((x, y, z) -> map((a, b, c) -> sin(a) + b / exp(-abs(c)), x, y, z), AT, A1, A2, A3)
        @test compare((x, y) -> mul!(x, 1, y), AT, A1, A2)
        @test compare((x, y) -> mul!(x, y, 1), AT, A1, A2)
    end
end

@testset "copy ($AT)" for AT in ATs
    N = 2
    for m1 in (0, 16, 32), m2 in (0, 16, 32), T in (Float32, ComplexF32)
        dims = (m1, m2)
        A1 = StridedView(rand(T, dims))
        A2 = StridedView(rand(T, dims))
        A3 = StridedView(rand(T, dims))
        for f2 in (identity, conj, adjoint, transpose), f1 in (identity, conj, transpose, adjoint)
            axes(f1(A1)) == axes(f2(A2)) || continue
            B1 = f1(copy(A1))
            B2 = f2(copy(A2))
            @test compare((x, y) -> copy!(y, x), AT, B1, B2)
        end
    end
end

@testset "broadcasting ($AT)" for AT in ATs
    for T in (Float32, ComplexF32)
        A0 = StridedView(rand(T, ()))
        A1 = StridedView(rand(T, (10,)))
        A2 = permutedims(StridedView(rand(T, (10, 10))), randperm(2))
        A3 = permutedims(StridedView(rand(T, (10, 10, 10))), randperm(3))
        A4 = StridedView(rand(T, (2, 0)))

        @test compare((x, y) -> x .+ sin.(y .- 3), AT, A1, A2)
        @test compare((y, z) -> y' .* z .- Ref(1 // 2), AT, A2, A3)
        @test compare((x, y, z) -> y' .* z .- max.(abs.(x), real.(z)), AT, A1, A2, A3)
        @test compare((u, y, z) -> y' .* z .- u, AT, A0, A2, A3)

        @test compare(x -> x .+ x, AT, A4)
    end
end

@testset "mapreduce ($AT)" for AT in ATs
    sz = 10
    N = 6
    for T in (Float32, ComplexF32)
        A1 = StridedView(rand(T, ntuple(Returns(sz), N)))

        @test compare(x -> sum(x; dims = (1, 3, 5)), AT, A1)
        @test compare(x -> mapreduce(sin, +, x; dims = (1, 3, 5)), AT, A1)
        @test compare(x -> sum(x; dims = (1, 3, 5)), AT, permutedims(A1, randperm(N)))
        @test compare(x -> mapreduce(sin, +, x; dims = (1, 3, 5)), AT, permutedims(A1, randperm(N)))

        A2 = sreshape(StridedView(rand(T, ntuple(Returns(sz), 3))), (sz, 1, 1, sz, sz, 1))

        @test compare((x, y) -> Strided._mapreducedim!(sin, +, identity, ntuple(Returns(sz), N), (x, y)), AT, A1, A2)
        @test compare((x, y) -> Strided._mapreducedim!(sin, +, Returns(0), ntuple(Returns(sz), N), (x, y)), AT, A1, A2)
        @test compare((x, y) -> Strided._mapreducedim!(sin, +, conj, ntuple(Returns(sz), N), (x, y)), AT, A1, A2)

        β = rand(T)
        @test compare((x, y) -> Strided._mapreducedim!(sin, +, a -> β, ntuple(Returns(sz), N), (x, y)), AT, A1, A2)
        @test compare((x, y) -> Strided._mapreducedim!(sin, +, a -> β * a, ntuple(Returns(sz), N), (x, y)), AT, A1, A2)
    end
end

@testset "reduce ($AT)" for AT in ATs
    N = 4
    for T in (Float32, ComplexF32)
        A1 = StridedView(rand(T, ntuple(Returns(10), N)))
        A2 = permutedims(StridedView(rand(T, ntuple(Returns(10), N))), randperm(N))
        @test compare(sum, AT, A1)
        @test compare(sum, AT, A2)
        @test compare(x -> maximum(real, x), AT, A1)
        @test compare(x -> maximum(abs, x), AT, A2)
        @test compare(x -> minimum(abs, x), AT, A1)
        @test compare(x -> minimum(real, x), AT, A2)

        A3 = StridedView(rand(T, (5, 5, 5)))
        @test compare(x -> prod(exp, x), AT, A3)
    end
end

@testset "0-dimensional (scalar) StridedView ($AT)" for AT in ATs
    @testset for T in (Float32, ComplexF32)
        R = fill(rand(T)) # 0-dimensional Array
        A = StridedView(AT(R))
        @test ndims(A) == 0

        # full reductions
        @test sum(A) == sum(R)
        @test prod(A) == prod(R)
        @test mapreduce(abs2, +, A) == mapreduce(abs2, +, R)
        @test maximum(abs, A) == maximum(abs, R)
        @test minimum(abs, A) == minimum(abs, R)
        @test sum(abs2, A) == sum(abs2, R)
        @test mapreduce(identity, +, A; init = one(T)) ==
            mapreduce(identity, +, R; init = one(T))

        # map / map! / copy! / fill!
        mapx = map(x -> 2x, A)
        GPUArrays.@allowscalar begin
            @test mapx[] == 2 * R[]
        end
        B = StridedView(AT(fill(zero(T))))
        map!(x -> x + one(T), B, A)
        GPUArrays.@allowscalar begin
            @test B[] == collect(R)[] + one(T)
        end
        copy!(B, A)
        GPUArrays.@allowscalar begin
            @test B[] == R[]
        end
        fill!(B, one(T))
        GPUArrays.@allowscalar begin
            @test B[] == one(T)
        end

        # offset handling: 0-dim views into a larger parent
        Psrc = AT(rand(T, 5))
        Pdst = AT(rand(T, 5))
        s = sreshape(StridedView(Psrc)[4:4], ())
        d = sreshape(StridedView(Pdst)[3:3], ())
        GPUArrays.@allowscalar begin
            @test sum(s) == Psrc[4]
        end
        copy!(d, s)
        GPUArrays.@allowscalar begin
            @test Pdst[3] == Psrc[4]
        end

        # low-level in-place reduction with a custom initop
        Pd = AT(rand(T, 5))
        d2 = sreshape(StridedView(Pd)[2:2], ())
        GPUArrays.@allowscalar begin
            prev = Pd[2]
        end
        Strided._mapreducedim!(sin, +, identity, (), (d2, A))
        GPUArrays.@allowscalar begin
            @test Pd[2] == prev + sin(R[])
        end
    end
end
