# Parse the CommonMark spec tests in spec.json (which come from
# https://spec.commonmark.org/0.31.2/spec.json) and check the Markdown
# stdlib against it. Also regenerate the test_spec.jl file from
# this, to be used in regression tests.
#
# There are two main reasons to run this script:
# 1. to adjust the tests after a change to the Markdown test suite
# 2. after updating spec.json
#

using Markdown
using JSON
using Test
using StyledStrings

#
# Some spec tests fail because of decisions made for Julia Markdown:
#
# - Even number of backticks in inline code for math mode: 17, 121, 329, 335, 336, 339, 349
# - Two or more spaces at the end of a line do *not* indicate a hard line break (see
#   <https://github.com/JuliaLang/julia/issues/16004>): 633, 635, 636, 638
#

function check_commonmark_spec_html1(tst; report::Bool=false, flavor::Symbol=:julia)
    input = tst["markdown"]
    parsed = Markdown.parse(input; flavor)
    expected = tst["html"]
    actual = Markdown.html(parsed)

    # extra: check that replacing Unix line ends (a single "line feed" also known as LF, '\n' or U+000A)
    # by classic macOS line ends (a single "carriage return", also known as CR, '\r' or U+000D) or
    # by Windows line ends (CR+LF) does not change how we parse things
    input2 = replace(input, "\n" => "\r\n")
    parsed2 = Markdown.parse(input2; flavor)
    parsed == parsed2 || println("test ", tst["example"], " is parsed differently with CRLF line ends")

    input3 = replace(input, "\n" => "\r")
    parsed3 = Markdown.parse(input3; flavor)
    parsed == parsed3 || println("test ", tst["example"], " is parsed differently with CR line ends")

    if expected != actual && report
        printstyled("============================================\n"; color=:cyan)
        println("failed test ", tst["example"], ":")
        print("input:\n'"); printstyled(input; color=:bold); println("'")
        print("parsed:\n'"); printstyled(parsed; color=:yellow); println("'")
        print("expected HTML:\n'"); printstyled(expected; color=:green); println("'")
        print("actual HTML:\n'"); printstyled(actual; color=:red); println("'")
        println()
    end
    return expected == actual
end

function check_commonmark_spec_html(data; report::Bool=false, flavor::Symbol=:julia)
    failed = Int[]
    for tst in data
        check_commonmark_spec_html1(tst; report, flavor) || push!(failed, tst["example"])
    end
    if report
        total = length(data)
        passed = total - length(failed)
        println("HTML score: $passed/$total = $(passed/total*100)% pass")
    end
    return BitSet(failed)
end

function check_commonmark_spec_roundtrip1(tst; report::Bool=false, flavor::Symbol=:julia)
    input = tst["markdown"]
    parsed = Markdown.parse(input; flavor)
    new_input = Markdown.plain(parsed)
    new_parsed = Markdown.parse(new_input; flavor)
    if parsed != new_parsed && report
        printstyled("============================================\n"; color=:cyan)
        println("failed test ", tst["example"], ":")
        print("input:\n'"); printstyled(input; color=:bold); println("'")
        print("parsed:\n'"); printstyled(parsed; color=:yellow); println("'")
        print("new_input:\n'"); printstyled(new_input; color=:green); println("'")
        print("new_parsed:\n'"); printstyled(new_parsed; color=:red); println("'")
        println()
    end
    return parsed == new_parsed
end

function check_commonmark_spec_roundtrip(data; report::Bool=false, flavor::Symbol=:julia)
    failed = Int[]
    for tst in data
        check_commonmark_spec_roundtrip1(tst; report, flavor) || push!(failed, tst["example"])
    end
    if report
        total = length(data)
        passed = total - length(failed)
        println("roundtrip score: $passed/$total = $(passed/total*100)% pass")
    end
    return BitSet(failed)
end

function escape_spec_string(s::String)
    s = Base.escape_string(s)
    # avoid string interpolation
    s = replace(s, "\$" => raw"\$")
    # encode non-breaking whitespace to make contrib/check-whitespace.jl happy
    s = replace(s, "\uA0" => raw"\u00A0")
    return s
end

function generate_test_file(io::IO, data, mode::Symbol; flavor::Symbol=:julia)
    # check all spec tests and record which are broken
    if mode == :HTML
        broken = check_commonmark_spec_html(data; flavor)
    elseif mode == :roundtrip
        broken = check_commonmark_spec_roundtrip(data; flavor)
    else
        error("unsupported mode $mode")
    end

    println(io, """
        # WARNING: this file was generated by $(basename(@__FILE__)), do not edit it directly.

        using Test
        using Markdown

        @testset "CommonMark spec test suite: $(mode) mode, $(flavor) flavor" begin
        """)

    section = ""
    for tst in data
        if section != tst["section"]
            section != "" && println(io, "end")  # end testset
            section = tst["section"]
            println(io)
            println(io, """
                #
                # $section
                #
                @testset "$section" begin
                """)
        end

        println(io, "    # Example ", tst["example"])
        println(io, "    input = \"", escape_spec_string(tst["markdown"]), "\"")
        if mode == :HTML
            println(io, "    md = Markdown.parse(input; flavor=:$(flavor))");
            println(io, "    expected = \"", escape_spec_string(tst["html"]), "\"")
            println(io, "    actual = Markdown.html(md)");
        elseif mode == :roundtrip
            println(io, "    expected = Markdown.parse(input; flavor=:$(flavor))");
            println(io, "    new_input = Markdown.plain(expected)");
            println(io, "    actual = Markdown.parse(new_input; flavor=:$(flavor))");
        else
            error("unsupported mode $mode")
        end
        if tst["example"] in broken
            println(io, "    @test_broken expected == actual");
        else
            println(io, "    @test expected == actual");
        end
        println(io)
    end
    section != "" && println(io, "end")  # end testset
    println(io)
    println(io, "end")
end

# parse the spec
commonmark_spec_tests = JSON.parsefile(joinpath(@__DIR__, "spec.json"))

for flavor in [:julia, :github, :common]
    # generate output file
    open(joinpath(@__DIR__, "test_spec_html_$(flavor).jl"), "w") do io
        generate_test_file(io, commonmark_spec_tests, :HTML; flavor)
    end

    # generate output file
    open(joinpath(@__DIR__, "test_spec_roundtrip_$(flavor).jl"), "w") do io
        generate_test_file(io, commonmark_spec_tests, :roundtrip; flavor)
    end

end
