onfail(body, x) = error("I might have overlooked something: $x")
onfail(body, _::Test.Pass) = nothing
onfail(body, _::Tuple{Test.Fail,T}) where {T} = body()

function is_ci()
    get(ENV, "TRAVIS", "") == "true" ||
    get(ENV, "APPVEYOR", "") in ("true", "True") ||
    get(ENV, "CI", "") in ("true", "True")
end

# Drop-in replacement for ImageMagick.load that bypasses the alpha-channel
# pre-association that ImageMagick_jll 7.x runs by default during pixel
# export. Without this bypass, transparent and low-α palette entries get
# zeroed/quantized before reaching the returned array, which is a
# representation choice unrelated to PNG decode correctness. Calling
# MagickSetImageAlphaChannel with the UndefinedAlphaChannel op (=0) flips
# the wand's alpha-channel state so subsequent MagickExportImagePixels
# returns the raw decoded samples instead. Also avoids the "Cannot parse
# colorspace LinearGRAY" path in ImageMagick.jl by not going through its
# `_metadata` colorspace-name lookup.
function _im_load_raw(fpath::AbstractString)
    wand = ImageMagick.MagickWand()
    ImageMagick.readimage(wand, fpath)
    ImageMagick.resetiterator(wand)
    # ccall library name must be a constant — reference ImageMagick_jll.libwand directly
    ccall((:MagickSetImageAlphaChannel, ImageMagick.ImageMagick_jll.libwand), Cint,
          (Ptr{Cvoid}, Cint), wand, Cint(0))  # 0 = UndefinedAlphaChannel
    w, h = ImageMagick.size(wand)
    has_alpha = ImageMagick.getimagealphachannel(wand)
    cs = ImageMagick.getimagecolorspace(wand)
    depth = ImageMagick.getimagedepth(wand)
    is_gray = startswith(cs, "Gray") || cs == "LinearGRAY"
    channelorder = is_gray ? (has_alpha ? "IA" : "I") :
                              (has_alpha ? "RGBA" : "RGB")
    n_chans = (is_gray ? 1 : 3) + (has_alpha ? 1 : 0)
    if depth <= 8
        buf = Array{UInt8}(undef, n_chans, w, h)
        storage_id = Cint(1)  # CharPixel
    else
        buf = Array{UInt16}(undef, n_chans, w, h)
        storage_id = Cint(7)  # ShortPixel
    end
    ccall((:MagickExportImagePixels, ImageMagick.ImageMagick_jll.libwand), Cint,
          (Ptr{Cvoid}, Cssize_t, Cssize_t, Csize_t, Csize_t, Ptr{UInt8}, Cint, Ptr{Cvoid}),
          wand, 0, 0, w, h, channelorder, storage_id, pointer(buf))
    T = depth <= 8 ? N0f8 : N0f16
    Tcol = is_gray ? (has_alpha ? GrayA{T} : Gray{T}) :
                     (has_alpha ? RGBA{T} : RGB{T})
    out = Array{Tcol}(undef, h, w)
    if is_gray && has_alpha
        @inbounds for j in 1:w, i in 1:h
            out[i,j] = GrayA{T}(reinterpret(T, buf[1,j,i]), reinterpret(T, buf[2,j,i]))
        end
    elseif is_gray
        @inbounds for j in 1:w, i in 1:h
            out[i,j] = Gray{T}(reinterpret(T, buf[1,j,i]))
        end
    elseif has_alpha
        @inbounds for j in 1:w, i in 1:h
            out[i,j] = RGBA{T}(reinterpret(T, buf[1,j,i]), reinterpret(T, buf[2,j,i]),
                                reinterpret(T, buf[3,j,i]), reinterpret(T, buf[4,j,i]))
        end
    else
        @inbounds for j in 1:w, i in 1:h
            out[i,j] = RGB{T}(reinterpret(T, buf[1,j,i]), reinterpret(T, buf[2,j,i]),
                              reinterpret(T, buf[3,j,i]))
        end
    end
    return out
end

absdiff(x, y) = x > y ? x - y : y - x

function imdiff(a, b)
    a_view = channelview(a)
    b_view = channelview(b)
    diffscale = max(maxabsfinite(a_view), maxabsfinite(b_view))
    d = sum(absdiff.(a_view, b_view))
    return d / (length(a) * diffscale)
end

_convert(C, T, xs::AbstractArray) =
    collect(colorview(C, map(i -> collect(reinterpret(T, collect(xs)[:, :, i])), 1:size(xs, 3))...))
_convert(C, T, xs::AbstractMatrix) = collect(colorview(C, collect(reinterpret(T, collect(xs)))))

_standardize_grayness(x) = x
_standardize_grayness(x::AbstractArray{<:Gray{Bool}}) = Gray{N0f8}.(x)
_standardize_grayness(x::AbstractArray{<:RGB}) = all(red.(x) .≈ green.(x) .≈ blue.(x)) ? Gray.(red.(x)) : x
_standardize_grayness(x::AbstractArray{<:RGBA}) = all(red.(x) .≈ green.(x) .≈ blue.(x)) ?
    GrayA.(colorview(GrayA, red.(x), alpha.(x))) :
    x


function plotdiffs(p, i)
    """
    Returns a Matrix of Colorants with following structure:
    --------------|---------------|----------------------|---------------------
    Orig Image p  | Orig Image i  | AbsDiff all channels | RelDiff all channels
    --------------|---------------|----------------------|---------------------
    1st channel p | 1st channel i | AbsDiff 1st channel  | RelDiff 1st channel
    --------------|---------------|----------------------|---------------------
    ...
    """
    _p = RGBA{Float64}.(p)
    __p = collect(channelview(_p))
    __p[4, :, :] .= 0.0
    _i = RGBA{Float64}.(i)
    __i = collect(channelview(_i))
    __i[4, :, :] .= 1.0 # make the difference in alpha be 1 so it is visible
    d = collect(colorview(RGBA{Float64}, absdiff.(__p, __i)))
    o = _plotdiffs(p, i)
    hborder = hcat(
        fill(RGBA{Float64}(0, 0, 0, 1), size(o, 1) + size(p, 1), 1),
        fill(RGBA{Float64}(1, 1, 1, 1), size(o, 1) + size(p, 1), 1),
        fill(RGBA{Float64}(0, 0, 0, 1), size(o, 1) + size(p, 1), 1),
    )

    collect(hcat(
        vcat(_p, _plotchannels(p)),
        hborder,
        vcat(_i, _plotchannels(i)),
        hborder,
        vcat(d, o),
        hborder,
        vcat(_rescale(d), _rescale(o)),
    ))
end

vborder(p) = vcat(
    fill(RGBA{Float64}(0, 0, 0, 1), 1, size(p, 2)),
    fill(RGBA{Float64}(1, 1, 1, 1), 1, size(p, 2)),
    fill(RGBA{Float64}(0, 0, 0, 1), 1, size(p, 2)),
)

function _plotdiffs(p, i)
    vcat(
        vborder(p),
        RGBA{Float64}.(absdiff.(red.(i), red.(p)), 0, 0, 1),
        vborder(p),
        RGBA{Float64}.(0, absdiff.(green.(i), green.(p)), 0, 1),
        vborder(p),
        RGBA{Float64}.(0, 0, absdiff.(blue.(i), blue.(p)), 1),
    )
end

function _plotdiffs(p::AbstractArray{<:RGBA}, i::AbstractArray{<:RGBA})
    a = absdiff.(alpha.(i), alpha.(p))
    vcat(
        vborder(p),
        RGBA{Float64}.(absdiff.(red.(i), red.(p)), 0, 0, 1),
        vborder(p),
        RGBA{Float64}.(0, absdiff.(green.(i), green.(p)), 0, 1),
        vborder(p),
        RGBA{Float64}.(0, 0, absdiff.(blue.(i), blue.(p)), 1),
        vborder(p),
        RGBA{Float64}.(a, a, a, 1),
    )
end

function _plotchannels(p)
    vcat(
        vborder(p),
        RGBA{Float64}.(red.(p), 0, 0, 1),
        vborder(p),
        RGBA{Float64}.(0, green.(p), 0, 1),
        vborder(p),
        RGBA{Float64}.(0, 0, blue.(p), 1),
    )
end

function _plotchannels(p::AbstractArray{<:RGBA})
    vcat(
        vborder(p),
        RGBA{Float64}.(red.(p), 0, 0, 1),
        vborder(p),
        RGBA{Float64}.(0, green.(p), 0, 1),
        vborder(p),
        RGBA{Float64}.(0, 0, blue.(p), 1),
        vborder(p),
        RGBA{Float64}.(alpha.(p), alpha.(p), alpha.(p), 1),
    )
end

function _rescale(x::AbstractArray)
    c = channelview(x)
    m = maximum(c)
    m = iszero(m) ? one(m) : m
    colorview(RGBA{Float64}, 100 * c ./ m)
end


## COPIED FROM Images.jl/src/algorithms.jl

"""
`m = maxabsfinite(A)` calculates the maximum absolute value in `A`, ignoring any values that are not finite (Inf or NaN).
"""
function maxabsfinite(A::AbstractArray{T}) where T
    ret = sentinel_min(typeof(abs(A[1])))
    for a in A
        ret = maxfinite_scalar(abs(a), ret)
    end
    ret
end

minfinite_scalar(a::T, b::T) where {T} = isfinite(a) ? (b < a ? b : a) : b
maxfinite_scalar(a::T, b::T) where {T} = isfinite(a) ? (b > a ? b : a) : b
minfinite_scalar(a::T, b::T) where {T<:Union{Integer,FixedPoint}} = b < a ? b : a
maxfinite_scalar(a::T, b::T) where {T<:Union{Integer,FixedPoint}} = b > a ? b : a
minfinite_scalar(a, b) = minfinite_scalar(promote(a, b)...)
maxfinite_scalar(a, b) = maxfinite_scalar(promote(a, b)...)

function minfinite_scalar(c1::C, c2::C) where C<:AbstractRGB
    C(minfinite_scalar(c1.r, c2.r),
      minfinite_scalar(c1.g, c2.g),
      minfinite_scalar(c1.b, c2.b))
end
function maxfinite_scalar(c1::C, c2::C) where C<:AbstractRGB
    C(maxfinite_scalar(c1.r, c2.r),
      maxfinite_scalar(c1.g, c2.g),
      maxfinite_scalar(c1.b, c2.b))
end

sentinel_min(::Type{T}) where {T<:Union{Integer,FixedPoint}} = typemax(T)
sentinel_max(::Type{T}) where {T<:Union{Integer,FixedPoint}} = typemin(T)
sentinel_min(::Type{T}) where {T<:AbstractFloat} = convert(T, NaN)
sentinel_max(::Type{T}) where {T<:AbstractFloat} = convert(T, NaN)
sentinel_min(::Type{C}) where {C<:AbstractRGB} = _sentinel_min(C, eltype(C))
_sentinel_min(::Type{C},::Type{T}) where {C<:AbstractRGB,T} = (s = sentinel_min(T); C(s,s,s))
sentinel_max(::Type{C}) where {C<:AbstractRGB} = _sentinel_max(C, eltype(C))
_sentinel_max(::Type{C},::Type{T}) where {C<:AbstractRGB,T} = (s = sentinel_max(T); C(s,s,s))
sentinel_min(::Type{C}) where {C<:AbstractGray} = _sentinel_min(C, eltype(C))
_sentinel_min(::Type{C},::Type{T}) where {C<:AbstractGray,T} = C(sentinel_min(T))
sentinel_max(::Type{C}) where {C<:AbstractGray} = _sentinel_max(C, eltype(C))
_sentinel_max(::Type{C},::Type{T}) where {C<:AbstractGray,T} = C(sentinel_max(T))
