package cjbind_test

import std.fs.{File, Path, exists}
import std.collection.collectArray
import std.unittest.*
import std.unittest.testmacro.{TestBuilder, Assert}
import glob
import cjbind.{generate}
import cjbind.options.{CjbindOptions, DefaultEnumStyle}
import cjbind.utils.formatString
import std.env.{getCommandLine, getVariable}

let testdataDir: String = if (exists("cjbind_test/testdata")) {
    "cjbind_test/testdata"
} else {
    "testdata"
}

func searchTestCases(): Array<String> {
    let paths = glob.glob("${testdataDir}/headers/*.h")
    return paths.map {p => p.toString()} |> collectArray
}

func applyTestOptions(opt: CjbindOptions, testcase: String): Unit {
    let content = String.fromUtf8(File.readFrom(Path(testcase)))
    for (line in content.split("\n")) {
        let trimmed = line.trimAscii()
        if (trimmed.startsWith("// cjbind-options:")) {
            let optsStr = trimmed["// cjbind-options:".size..].trimAscii()
            let parts = optsStr.split(" ", removeEmpty: true)
            var i = 0
            while (i < parts.size) {
                let part = parts[i].trimAscii()
                if (part == "--default-enum-style" && i + 1 < parts.size) {
                    opt.defaultEnumStyle = match (parts[i + 1].trimAscii()) {
                        case "newtype" => DefaultEnumStyle.NewType
                        case _ => DefaultEnumStyle.Consts
                    }
                    i += 2
                } else if (part == "--objc") {
                    opt.objc = true
                    i += 1
                } else if (part == "--no-enum-prefix") {
                    opt.noEnumPrefix = true
                    i += 1
                } else if (part == "--fit-macro-constants") {
                    opt.fitMacroConstants = true
                    i += 1
                } else if (part == "--no-union-accessor-workaround") {
                    opt.useUnionAccessorWorkaround = false
                    i += 1
                } else if (part == "--clang-arg" && i + 1 < parts.size) {
                    opt.clangArgs.add(parts[i + 1].trimAscii())
                    i += 2
                } else {
                    eprintln("Warning: unrecognized test option: ${part}")
                    i += 1
                }
            }
        }
    }
}

let defaultTarget = "--target=x86_64-unknown-linux-gnu"

func hasTargetFlag(opt: CjbindOptions): Bool {
    for (arg in opt.clangArgs) {
        if (arg.startsWith("--target=")) {
            return true
        }
    }
    return false
}

func generateOutput(testcase: String): String {
    let opt = CjbindOptions()
    opt.headers.add(testcase)
    opt.noDetectIncludePath = true
    applyTestOptions(opt, testcase)
    // Pin to a default target so that expected outputs are platform-independent,
    // like rust-bindgen does.
    if (!hasTargetFlag(opt)) {
        opt.clangArgs.add(defaultTarget)
    }
    return formatString(generate(opt))
}

// 如果 args 中有 update,则更新 testcase 的输出
// 否则比较 testcase 的输出和 expected 文件
// 输出应该在 testcase 目录下的 expected 目录中
func processTestCase(testcase: String): Unit {
    let headerPath = Path(testcase)
    let headerName = headerPath.fileName.toString()
    let output = generateOutput(testcase)

    var dotIdx = headerName.size
    var di = headerName.size - 1
    while (di >= 0) {
        if (headerName[di] == 46u8) {
            dotIdx = di
            break
        }
        di -= 1
    }
    let baseName = headerName[0..dotIdx]
    let expectedPath = Path("${testdataDir}/expected/${baseName}.cj")

    let args = getCommandLine()
    let shouldUpdate = getVariable("CJBIND_UPDATE_EXPECTED").isSome() || args.any { arg: String => arg == "update" }

    let normalizedOutput = output.replace("\r\n", "\n")

    if (shouldUpdate) {
        File.writeTo(expectedPath, normalizedOutput.toArray())
    } else {
        let expected = String.fromUtf8(File.readFrom(expectedPath)).replace("\r\n", "\n")
        @Assert(normalizedOutput, expected)
    }
}

@TestBuilder
public func buildOutputTestSuite(): TestSuite {
    let testcases = searchTestCases()

    let suiteBuilder = TestSuite.builder("Output")
    suiteBuilder.add(UnitTestCase.createParameterized("Test cjbind output", testcases, body: processTestCase))
    suiteBuilder.build()
}