# This file is a part of Julia. License is MIT: https://julialang.org/license

using Test, Base.BinaryPlatforms, Base.BinaryPlatforms.CPUID

@testset "CPUID" begin
    @test CPUID.cpu_isa() isa CPUID.ISA

    get_x86_64(n) = (CPUID.ISAs_by_family["x86_64"][n].second)
    @test get_x86_64(2) <  get_x86_64(4)
    @test get_x86_64(5) <= get_x86_64(5)
    @test get_x86_64(3) >= get_x86_64(3)
    @test get_x86_64(7) >= get_x86_64(1)
    @test sort([get_x86_64(6), get_x86_64(4), get_x86_64(2), get_x86_64(4)]) ==
        [get_x86_64(2), get_x86_64(4), get_x86_64(4), get_x86_64(6)]
end

# Helper constructor to create a Platform with `validate_strict` set to `true`.
P(args...; kwargs...) = Platform(args...; validate_strict=true, kwargs...)

# Ensure the platform type constructors are well behaved
@testset "Platform validation" begin
    for os_name in ("Linux", "linux", "FREEBSD", "wiNDows", "MacOS")
        p = P("x86_64", os_name)
        @test isa(p, Platform)
        @test os(p) == lowercase(os_name)
    end
    for arch_name in ("x86_64", "ARMv7l", "armv6l", "I686", "pOWerpc64le")
        p = P(arch_name, "linux")
        @test isa(p, Platform)
        @test arch(p) == lowercase(arch_name)
    end
    # Test some normalization
    @test arch(P("amd64", "freebsd")) == "x86_64"
    @test arch(P("ARM", "linux")) == "armv7l"
    @test arch(P("ppc64le", "linux")) == "powerpc64le"

    # test some error cases
    @test_throws ArgumentError P("x64_86", "linux")
    @test_throws ArgumentError P("x86_64", "not_an_os")
    @test_throws ArgumentError P("x86_64", "linux"; libc="crazy_libc")
    @test_throws ArgumentError P("x86_64", "linux"; libc="glibc", call_abi="crazy_abi")
    @test_throws ArgumentError P("x86_64", "linux"; libc="glibc", call_abi="eabihf")
    @test_throws ArgumentError P("i686", "linux"; libc="musl", call_abi="eabi")
    @test_throws ArgumentError P("arm", "linux"; call_abi="")
    @test_throws ArgumentError P("armv7l", "linux"; call_abi="kekeke")
    @test_throws ArgumentError P("armv6l", "linux"; call_abi="kekeke")
    @test_throws ArgumentError P("armv6l", "linux"; libgfortran_version="lel")
    @test_throws ArgumentError P("x86_64", "linux"; cxxstring_abi="lel")
    @test_throws ArgumentError P("x86_64", "windows"; libstdcxx_version="lel")
    @test_throws ArgumentError P("i686", "macos")
    @test_throws ArgumentError P("x86_64", "macos"; libc="glibc")
    @test_throws ArgumentError P("x86_64", "macos"; call_abi="eabihf")
    @test_throws ArgumentError P("powerpc64le", "windows")
    @test_throws ArgumentError P("x86_64", "windows"; call_abi="eabihf")
    @test_throws ArgumentError P("x86_64", "freebsd"; libc="crazy_libc")
    @test_throws ArgumentError P("x86_64", "freebsd"; call_abi="crazy_abi")
    @test_throws ArgumentError P("x86_64", "freebsd"; call_abi="eabihf")
    @test_throws ArgumentError P("x86_64", "linux"; arch="i686")
    @test_throws ArgumentError P("x86_64", "linux"; ARCH="i686")
    @test_throws ArgumentError P("x86_64", "linux"; os="windows")
end

@testset "Platform properties" begin
    # Test that `platform_name()` works
    platforms = ("Linux", "macOS", "Windows", "FreeBSD")
    for platform in platforms
        @test platform_name(P("x86_64", platform)) == platform
    end

    # Test `arch()`
    arch_names = ("x86_64", "i686", "powerpc64le", "armv7l", "armv6l", "aarch64")
    for arch_name in arch_names
        @test arch(P(arch_name, "linux")) == arch_name
    end

    # Test that if we aren't using strict validation, we can actually use new names too:
    @test arch(Platform("jpu", "linux")) == "jpu"

    # platform_dlext()
    @test platform_dlext(P("x86_64", "linux")) == "so"
    @test platform_dlext(P("armv7l", "windows")) == "dll"
    @test platform_dlext(P("x86_64", "freebsd")) == "so"
    @test platform_dlext(P("aarch64", "macos")) == "dylib"
    @test platform_dlext() == platform_dlext(HostPlatform())

    # wordsize()
    @test wordsize(P("i686", "linux")) == wordsize(P("armv7l", "windows")) == 32
    @test wordsize(P("aarch64", "macos")) == wordsize(P("x86_64", "freebsd")) == 64
    @test wordsize(P("x86_64", "windows")) == wordsize(P("powerpc64le", "linux")) == 64

    # call_abi()
    for platform in platforms
        @test call_abi(P("x86_64", platform)) === nothing
    end
    @test call_abi(P("armv7l", "linux")) == "eabihf"
    @test call_abi(P("armv7l", "linux"; call_abi="eabihf")) == "eabihf"
    @test call_abi(P("armv7l", "linux"; call_abi="eabi")) == "eabi"
    @test call_abi(P("armv6l", "linux")) == "eabihf"
    # Test that we can at least set an `eabi` call ABI, not that Julia actually supports it...
    @test call_abi(P("armv7l", "linux"; call_abi="eabi")) == "eabi"

    # Test some different OS's and libc/call ABIs
    @test triplet(P("i686", "Windows")) == "i686-w64-mingw32"
    @test triplet(P("x86_64", "linux"; libc="musl")) == "x86_64-linux-musl"
    @test triplet(P("armv7l", "linux"; libc="musl")) == "armv7l-linux-musleabihf"
    @test triplet(P("armv6l", "linux"; libc="musl", call_abi="eabihf")) == "armv6l-linux-musleabihf"
    @test triplet(P("armv6l", "linux"; call_abi="eabi")) == "armv6l-linux-gnueabi"
    @test triplet(P("x86_64", "linux")) == "x86_64-linux-gnu"
    @test triplet(P("armv6l", "linux")) == "armv6l-linux-gnueabihf"
    @test triplet(P("x86_64", "macos")) == "x86_64-apple-darwin"
    @test triplet(P("x86_64", "macos"; os_version=v"16")) == "x86_64-apple-darwin16"
    @test triplet(P("x86_64", "freebsd")) == "x86_64-unknown-freebsd"
    @test triplet(P("i686", "freebsd")) == "i686-unknown-freebsd"

    # Now test libgfortran/cxxstring ABIs
    @test triplet(P("x86_64", "linux"; libgfortran_version=v"3", cxxstring_abi="cxx11")) == "x86_64-linux-gnu-libgfortran3-cxx11"
    @test triplet(P("armv7l", "linux"; libc="musl", cxxstring_abi="cxx03")) == "armv7l-linux-musleabihf-cxx03"
    if !isnothing(detect_libgfortran_version())
        # When `libgfortran` can be detected at runtime, make sure
        # `HostPlatform` has the appropriate key.
        @test tags(HostPlatform())["libgfortran_version"] == string(detect_libgfortran_version())
    end

    # Test tags()
    t = tags(P("x86_64", "linux"))
    @test all(haskey.(Ref(t), ("arch", "os", "libc")))
    @test haskey(tags(P("x86_64", "linux"; customtag="foo")), "customtag")
    @test tags(HostPlatform())["julia_version"] == string(VERSION.major, ".", VERSION.minor, ".", VERSION.patch)

    # Test that we can modify tags at will using the dict-like interface:
    p = P("x86_64", "linux")
    p["foo"] = "bar"
    @test tags(p)["foo"] == "bar"
    @test p["foo"] == "bar"
    @test p["os"] == "linux"
    p["os"] = "JuliaOS"
    @test p["os"] == "juliaos"

    # Test that trying to set illegal tags fails
    @test_throws ArgumentError p["os"] = "a+b"

    # Test that our `hash()` is stable
    @test hash(HostPlatform()) == hash(HostPlatform())

    # Test that round-tripping through `triplet` for a does not
    # maintain equality, as we end up losing the `compare_strategies`:
    p = Platform("x86_64", "linux"; cuda = v"11")
    Base.BinaryPlatforms.set_compare_strategy!(p, "cuda", Base.BinaryPlatforms.compare_version_cap)
    q = parse(Platform, triplet(p))
    @test q != p
end

@testset "Triplet parsing" begin
    # Make sure the Platform() with explicit triplet works
    R(str) = parse(Platform, str; validate_strict=true)
    @test R("x86_64-linux-gnu") == P("x86_64", "linux")
    @test R("x86_64-linux-musl") == P("x86_64", "linux"; libc="musl")
    @test R("i686-unknown-linux-gnu") == P("i686", "linux")
    @test R("x86_64-apple-darwin") == P("x86_64", "macos")
    @test R("x86_64-apple-darwin14") == P("x86_64", "macos"; os_version="14")
    @test R("x86_64-apple-darwin17.0.0") == P("x86_64", "macos"; os_version="17")
    @test R("armv7l-pc-linux-gnueabihf") == P("armv7l", "linux")
    @test R("armv7l-linux-musleabihf") == P("armv7l", "linux"; libc="musl")
    @test R("armv6l-linux-gnueabi") == P("armv6l", "linux"; call_abi="eabi")
    # Test that the short name "arm" goes to `armv7l`
    @test R("arm-linux-gnueabihf") == P("armv7l", "linux")
    @test R("aarch64-unknown-linux-gnu") == P("aarch64", "linux")
    @test R("powerpc64le-linux-gnu") == P("powerpc64le", "linux")
    @test R("ppc64le-linux-gnu") == P("powerpc64le", "linux")
    @test R("x86_64-w64-mingw32") == P("x86_64", "windows")
    @test R("i686-w64-mingw32") == P("i686", "windows")

    # FreeBSD has lots of arch names that don't match elsewhere
    @test R("x86_64-unknown-freebsd11.1") == P("x86_64", "freebsd"; os_version=v"11.1")
    @test R("i686-unknown-freebsd11.1") == P("i686", "freebsd"; os_version=v"11.1")
    @test R("amd64-unknown-freebsd12.0") == P("x86_64", "freebsd"; os_version=v"12.0")
    @test R("i386-unknown-freebsd10.3") == P("i686", "freebsd"; os_version=v"10.3")
    @test R("aarch64-apple-darwin18.7") == P("aarch64", "macos"; os_version=v"18.7")
    @test R("arm64-apple-darwin20") == P("aarch64", "macos"; os_version=v"20")

    # Test inclusion of ABI stuff, both old-style and new-style
    @test R("x86_64-linux-gnu-gcc7") == P("x86_64", "linux"; libgfortran_version=v"4")
    @test R("x86_64-linux-gnu-gcc4-cxx11") == P("x86_64", "linux"; libgfortran_version=v"3", cxxstring_abi="cxx11")
    @test R("x86_64-linux-gnu-cxx11") == P("x86_64", "linux"; cxxstring_abi="cxx11")
    @test R("x86_64-linux-gnu-libgfortran3-cxx03") == P("x86_64", "linux"; libgfortran_version=v"3", cxxstring_abi="cxx03")
    @test R("x86_64-linux-gnu-libstdcxx26") ==  P("x86_64", "linux"; libstdcxx_version=v"3.4.26")

    @test_throws ArgumentError R("totally FREEFORM text!!1!!!1!")
    @test_throws ArgumentError R("invalid-triplet-here")
    @test parse(Platform, "aarch64-linux-gnueabihf") == Platform("aarch64", "linux"; call_abi="eabihf")
    @test_throws ArgumentError R("aarch64-linux-gnueabihf")
    @test_throws ArgumentError R("x86_64-w32-mingw64")

    # Test extended attributes
    @test R("x86_64-linux-gnu-march+avx2") == P("x86_64", "linux"; march="avx2")
    @test R("x86_64-linux-gnu-march+x86_64-cuda+10.1") == P("x86_64", "linux"; march="x86_64", cuda="10.1")

    # Round-trip our little homie through `triplet()`, with some bending
    # of the rules for MacOS and FreeBSD, who have incomplete `os_version`
    # numbers embedded within their triplets.
    p = Platform("x86_64", "linux")
    @test parse(Platform, triplet(p)) == p

    # Also test round-tripping through `repr()`:
    p = Platform("aarch64", "macos"; os_version=v"20", march="armv8_4_crypto_sve")
    @test eval(Meta.parse(repr(p))) == p
end

@testset "platforms_match()" begin
    # Just do a quick combinatorial sweep for completeness' sake for platform matching
    linux = P("x86_64", "linux")
    for libgfortran_version in (nothing, v"3", v"5"),
        libstdcxx_version in (nothing, v"3.4.18", v"3.4.26"),
        cxxstring_abi in (nothing, :cxx03, :cxx11)

        p = P("x86_64", "linux"; libgfortran_version, libstdcxx_version, cxxstring_abi)
        @test platforms_match(linux, p)
        @test platforms_match(p, linux)

        # Also test auto-string-parsing
        @test platforms_match(triplet(linux), p)
        @test platforms_match(linux, triplet(p))
    end

    # Test that Julia version is matched only on major.minor by default
    @test platforms_match(P("x86_64", "linux"; julia_version=v"1.5.0"),
                          P("x86_64", "linux"; julia_version=v"1.5.1"))
    @test !platforms_match(P("x86_64", "linux"; julia_version=v"1.5.0"),
                           P("x86_64", "linux"; julia_version=v"1.6.0"))

    # Ensure many of these things do NOT match
    @test !platforms_match(linux, P("i686", "linux"))
    @test !platforms_match(linux, P("x86_64", "windows"))
    @test !platforms_match(linux, P("x86_64", "macos"))

    # Make some explicitly non-matching compiler ABI platforms
    host = P("x86_64", "linux"; libgfortran_version=v"5", cxxstring_abi="cxx11")
    for arch in ("x86_64", "i686", "aarch64", "armv6l", "armv7l", "powerpc64le"),
        kwargs in ((:libgfortran_version => v"3",), (:cxxstring_abi => "cxx03",),
                   (:libgfortran_version => v"4", :cxxstring_abi => "cxx11"),
                   (:libgfortran_version => v"3", :cxxstring_abi => "cxx03"))
        a = P(arch, "linux"; libgfortran_version=v"5", cxxstring_abi="cxx11")
        b = P(arch, "linux"; kwargs...)
        @test !platforms_match(a, b)
    end

    # Test version bounds with HostPlatform()
    host = HostPlatform(P("x86_64", "macos"; os_version="14", libstdcxx_version=v"3.4.26"))
    @test platforms_match(host, P("x86_64", "macos"))
    @test platforms_match(host, P("x86_64", "macos"; os_version="14"))
    @test platforms_match(host, P("x86_64", "macos"; os_version="13"))
    @test !platforms_match(host, P("x86_64", "macos"; os_version="15"))
    @test platforms_match(host, P("x86_64", "macos"; libstdcxx_version="3.4.18"))
    @test platforms_match(host, P("x86_64", "macos"; os_version=v"10", libstdcxx_version="3.4.18"))
    @test !platforms_match(host, P("x86_64", "macos"; os_version=v"10", libstdcxx_version="3.4.27"))
    @test !platforms_match(host, P("x86_64", "macos"; os_version=v"14", libstdcxx_version=v"4"))
end

@testset "DL name/version parsing" begin
    # Make sure our version parsing code is working
    @test parse_dl_name_version("libgfortran.so", "linux") == ("libgfortran", nothing)
    @test parse_dl_name_version("libgfortran.so.3", "linux") == ("libgfortran", v"3")
    @test parse_dl_name_version("libgfortran.so.3.4", "linux") == ("libgfortran", v"3.4")
    @test_throws ArgumentError parse_dl_name_version("libgfortran.so.3.4a", "linux")
    @test_throws ArgumentError parse_dl_name_version("libgfortran", "linux")
    @test_throws ArgumentError parse_dl_name_version("libgfortranso", "linux")
    @test parse_dl_name_version("libgfortran.so", "freebsd") == ("libgfortran", nothing)
    @test parse_dl_name_version("libgfortran.so.3", "freebsd") == ("libgfortran", v"3")
    @test parse_dl_name_version("libgfortran.so.3.4", "freebsd") == ("libgfortran", v"3.4")
    @test_throws ArgumentError parse_dl_name_version("libgfortran.so.3.4a", "freebsd")
    @test_throws ArgumentError parse_dl_name_version("libgfortran", "freebsd")
    @test_throws ArgumentError parse_dl_name_version("libgfortranso", "freebsd")
    @test parse_dl_name_version("libgfortran.dylib", "macos") == ("libgfortran", nothing)
    @test parse_dl_name_version("libgfortran.3.dylib", "macos") == ("libgfortran", v"3")
    @test parse_dl_name_version("libgfortran.3.4.dylib", "macos") == ("libgfortran", v"3.4")
    @test parse_dl_name_version("libgfortran.3.4a.dylib", "macos") == ("libgfortran.3.4a", nothing)
    @test_throws ArgumentError parse_dl_name_version("libgfortran", "macos")
    @test_throws ArgumentError parse_dl_name_version("libgfortrandylib", "macos")
    @test parse_dl_name_version("libgfortran.dll", "windows") == ("libgfortran", nothing)
    @test parse_dl_name_version("libgfortran-3.dll", "windows") == ("libgfortran", v"3")
    @test parse_dl_name_version("libgfortran-3.4.dll", "windows") == ("libgfortran", v"3.4")
    @test parse_dl_name_version("libgfortran-3.4a.dll", "windows") == ("libgfortran-3.4a", nothing)
    @test_throws ArgumentError parse_dl_name_version("libgfortran", "windows")
    @test_throws ArgumentError parse_dl_name_version("libgfortrandll", "windows")
end

@testset "Sys.is* overloading" begin
    # Test that we can indeed ask if something is linux or windows, etc...
    @test Sys.islinux(P("aarch64", "linux"))
    @test !Sys.islinux(P("x86_64", "windows"))
    @test Sys.iswindows(P("i686", "windows"))
    @test !Sys.iswindows(P("powerpc64le", "linux"))
    @test Sys.isapple(P("x86_64", "macos"))
    @test !Sys.isapple(P("armv7l", "windows"))
    @test Sys.isbsd(P("aarch64", "macos"))
    @test Sys.isbsd(P("x86_64", "freebsd"))
    @test !Sys.isbsd(P("x86_64", "linux"; libc="musl"))
end

@testset "Compiler ABI detection" begin
    # Let's check and ensure that we can autodetect the currently-running Julia process
    @test detect_libgfortran_version() !== nothing

    # We run these to get coverage, but we can't test anything, because we could be built
    # with `clang`, which wouldn't have any `libstdc++` constraints at all
    detect_libstdcxx_version()
    detect_cxxstring_abi()
end

@testset "select_platform" begin
    platforms = Dict(
        # Typical binning test
        P("x86_64", "linux"; libgfortran_version=v"3") => "linux4",
        P("x86_64", "linux"; libgfortran_version=v"4") => "linux7",
        P("x86_64", "linux"; libgfortran_version=v"5") => "linux8",

        # Ambiguity test
        P("aarch64", "linux"; libgfortran_version=v"3") => "linux3",
        P("aarch64", "linux"; libgfortran_version=v"3", libstdcxx_version=v"3.4.18") => "linux5",
        P("aarch64", "linux"; libgfortran_version=v"3", libstdcxx_version=v"3.4.18", foo="bar") => "linux9",

        # OS test
        P("x86_64", "macos"; libgfortran_version=v"3") => "mac4",
        P("x86_64", "windows"; cxxstring_abi=:cxx11) => "win",
    )

    @test select_platform(platforms, P("x86_64", "linux")) == "linux8"
    @test select_platform(platforms, P("x86_64", "linux"; libgfortran_version=v"4")) == "linux7"

    # Ambiguity test
    @test select_platform(platforms, P("aarch64", "linux")) == "linux3"
    @test select_platform(platforms, P("aarch64", "linux"; libgfortran_version=v"3")) == "linux3"
    @test select_platform(platforms, P("aarch64", "linux"; libgfortran_version=v"3", libstdcxx_version=v"3.4.18")) === "linux5"
    @test select_platform(platforms, P("aarch64", "linux"; libgfortran_version=v"4")) === nothing

    @test select_platform(platforms, P("x86_64", "macos")) == "mac4"
    @test select_platform(platforms, P("x86_64", "macos"; libgfortran_version=v"4")) === nothing

    @test select_platform(platforms, P("x86_64", "windows"; cxxstring_abi="cxx11")) == "win"
    @test select_platform(platforms, P("x86_64", "windows"; cxxstring_abi="cxx03")) === nothing

    # Sorry, Alex. ;)
    @test select_platform(platforms, P("x86_64", "freebsd")) === nothing

    # The new "most complete match" algorithm deals with ambiguities as follows:
    platforms = Dict(
        P("x86_64", "linux") => "normal",
        P("x86_64", "linux"; sanitize="memory") => "sanitized",
    )
    @test select_platform(platforms, P("x86_64", "linux")) == "normal"
    @test select_platform(platforms, P("x86_64", "linux"; sanitize="memory")) == "sanitized"

    # Ties are broken by reverse-sorting by triplet:
    platforms = Dict(
        P("x86_64", "linux"; libgfortran_version=v"3") => "libgfortran3",
        P("x86_64", "linux"; libgfortran_version=v"4") => "libgfortran4",
    )
    @test select_platform(platforms, P("x86_64", "linux")) == "libgfortran4"
    @test select_platform(platforms, P("x86_64", "linux"; libgfortran_version=v"3")) == "libgfortran3"
end

@testset "Custom comparators" begin
    # We're going to define here some custom comparators for Platform objects to ensure they work.
    # First, a symmetric one, which doesn't care which `Platform` object requested this comparison:
    function matches_oddness(a::String, b::String, a_requested::Bool, b_requested::Bool)
        return (parse(Int, a) % 2) == (parse(Int, b) % 2)
    end

    comp_strat = Dict("vally" => matches_oddness)

    # First, test that these two do not match, because it's using equality to test the `vally` tag
    a = Platform("x86_64", "linux"; vally="2")
    b = Platform("x86_64", "linux"; vally="4")
    @test !platforms_match(a, b)

    # Now, test that setting one or both `Platform`'s to use the `matches_oddness()` comparator works:
    ac = Platform("x86_64", "linux"; vally="2", compare_strategies=comp_strat)
    bc = Platform("x86_64", "linux"; vally="4", compare_strategies=comp_strat)
    @test platforms_match(ac, b)
    @test platforms_match(a, bc)
    @test platforms_match(ac, bc)

    # Test that even with the comparison strat, we don't match if they're not both even:
    bfc = Platform("x86_64", "linux"; vally="3", compare_strategies=comp_strat)
    @test !platforms_match(ac, bfc)


    # Next, an asymmetric comparison strategy.  We'll create a "less than or equal to" constraint
    # that uses the `{a,b}_requested` parameters to determine which number represents the limit.
    function less_than_constraint(a::String, b::String, a_requested::Bool, b_requested::Bool)
        a = parse(Int, a)
        b = parse(Int, b)
        if a_requested && !b_requested
            return b < a
        end
        if b_requested && !a_requested
            return a < b
        end
        # If two constraints have been requested, return true if they are the same constraint.
        return a == b
    end

    comp_strat = Dict("vally" => less_than_constraint)
    a = Platform("x86_64", "linux"; vally="2")
    b = Platform("x86_64", "linux"; vally="4")

    ac = Platform("x86_64", "linux"; vally="2", compare_strategies=comp_strat)
    bc = Platform("x86_64", "linux"; vally="4", compare_strategies=comp_strat)

    # Vanilla comparison doesn't work
    @test !platforms_match(a, b)

    # a and bc match, but not ac and b.  Also test reciprocity.
    @test platforms_match(a, bc)
    @test platforms_match(bc, a)
    @test !platforms_match(ac, b)
    @test !platforms_match(b, ac)

    # ac and bc do not match, but ac and ac do
    @test !platforms_match(ac, bc)
    @test platforms_match(ac, ac)
    @test platforms_match(bc, bc)
end
