/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
 * This source file is part of the Cangjie project, licensed under Apache-2.0
 * with Runtime Library Exception.
 *
 * See https://cangjie-lang.cn/pages/LICENSE for license information.
 */

// The Cangjie API is in Beta. For details on its capabilities and limitations, please refer to the README file.

package std.unittest

import std.process.*
import std.sync.AtomicOptionReference
import std.fs.*
@When[os == "Windows"]
import std.convert.Parsable
import std.collection.*
import std.random.Random
import std.sync.ReentrantMutex

private let TEMP_DIR = TempDirectory()

struct WrappedProcess {
    WrappedProcess(
        private let process: SubProcess,
        private let stdOutFile: ?Path,
        private let stdErrFile: ?Path
    ) {}

    private let status = AtomicOptionReference<CapturedProcessResult>()

    func wait(): Int64 {
        let exitCode = process.wait()
        if (status.load().isSome()) {
            return exitCode
        }

        let stdOut = readCapturedFrom(stdOutFile)
        let stdErr = readCapturedFrom(stdErrFile)
        let result = CapturedProcessResult(stdOut, stdErr)
        this.status.compareAndSwap(None, Some(result))

        return exitCode
    }

    func terminate(): Unit {
        if (status.load().isSome()) {
            return
        }

        process.terminateSilent(force: true)
    }

    func stdout(): Array<Byte> {
        status.load()?.stdOut ?? []
    }

    func stderr(): Array<Byte> {
        status.load()?.stdErr ?? []
    }
}

private class CapturedProcessResult {
    CapturedProcessResult(
        let stdOut: Array<Byte>,
        let stdErr: Array<Byte>
    ) {}
}

// for tests only, Process.start can't be invoked from tests
func processStart(ctx: MainExecutionCtx): WrappedProcess {
    Process.start(ctx)
}

extend Process {
    static func start(ctx: MainExecutionCtx): WrappedProcess {
        let stdout = captureFor(ctx)
        let stderr = captureFor(ctx)
        let cmd = ctx.executeCommand

        let process = try {
            launch(
                cmd.command, cmd.args.toArray(), environment: cmd.env,
                stdOut: redirectFor(stdout), stdErr: redirectFor(stderr)
            )
        } catch (cause: Exception) {
            stdout?.close() // std.process does consume these two
            stderr?.close() // so we only close them in the case of error
            throw cause
        }

        return WrappedProcess(process, stdout?.info.path, stderr?.info.path)
    }

    // NOTE: we never use ProcessRedirect.Pipe because it can't be used
    // with current std.process due to implementation issues
    // causing coroutines scheduler stagnation
    private static func captureFor(ctx: MainExecutionCtx): ?File {
        match (ctx.outputReporter.capture) {
            case true => TEMP_DIR.createTempFile(prefix: "test-worker-", suffix: ".stdout")
            case _ => None
        }
    }

    private static func redirectFor(file: ?File): ProcessRedirect {
        match (file) {
            case Some(file) => FromFile(file)
            case None => Inherit
        }
    }
}

extend SubProcess {
    
    func terminateSilent(force!: Bool): Unit {
        if (isAlive()) {
            // on Linux it's always alive until we invoke waitpid()
            // however on Windows process dies concurrently
            // so we do have a race here and there is a chance to kill something
            // irrelevant but we have no better solution as it's better than nothing
            // this must be fixed in std.process and the only workaround is to
            // use isAlive() and catch exceptions
            try {
                terminate(force: force)
            } catch (cause: Exception) {
                eprintln("Failed to terminate worker process ${pid}: ${cause}")
            }
        }
    }
}

private func removeFileSilent(path: Path): Unit {
    try {
        remove(path)
    } catch (_) {
        // ignore, we don't care
    }
}

private func readCapturedFrom(path: ?Path): Array<Byte> {
    match (path) {
        case Some(path) where exists(path) =>
            try {
                File.readFrom(path)
            } catch (e: Exception) {
                "Failed to read capture file ${path}: ${e}".toArray()
            } finally {
                removeFileSilent(path)
            }
        case _ => Array<Byte>()
    }
}