/*
 * 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.unittest.common.*
import std.time.DateTime
import std.collection.*

interface Result <: Serializable<Result> {
    // maybe rename to visitSubResults?
    func visitChildren(_: (Result) -> Unit): Unit {}

    func walk(visitor: (Result) -> Unit): Unit {
        visitor(this)
        visitChildren{ x => x.walk(visitor) }
    }

    func serializeInternal(): DataModel {
        match (this) {
            case step: RunStepResult =>
                step.doSerialize().add(field<String>("kind", "step"))
            case testCase: TestCaseResult =>
                testCase.doSerialize().add(field<String>("kind", "case"))
            case suite: TestSuiteResult =>
                suite.doSerialize().add(field<String>("kind", "suite"))
            case group: TestGroupResult =>
                group.doSerialize().add(field<String>("kind", "group"))
            case _ => throw Exception("Required due to bug in compiler")
        }
    }

    static func deserialize(dm: DataModel): Result {
        let dms = dm.asStruct()
        let kind = String.deserialize(dms.get("kind"))
        match (kind) {
            case "step" => RunStepResult.doDeserialize(dms)
            case "case" => TestCaseResult.doDeserialize(dms)
            case "suite" => TestSuiteResult.doDeserialize(dms)
            case "group" => TestGroupResult.doDeserialize(dms)
            case _ => throw Exception("invalid kind ${kind}")
        }
    }

    prop details: Details
}

abstract class ResultContainer<T> <: Result where T <: Result {
    let subResults = ArrayList<T>()

    public func visitChildren(visitor: (Result) -> Unit): Unit {
        for (r in subResults) {
            visitor(r)
        }
    }

    public open func add(child: T) {
        if (!finished) {
            subResults.add(child)
        } else {
            throw IllegalStateException("Should only be called while collecting results")
        }
    }

    protected var _details: Details = Details()
    private var _detailsComputed = false
    private var finished: Bool = false

    prop isFinished: Bool {
        get() {
            finished
        }
    }

    // lazily calculated after execution is finished
    public prop details: Details {
        get() {
            if (_detailsComputed) { return _details }

            if (!finished) {
                throw IllegalStateException("Should be called only after test execution is finished.")
            }

            visitChildren { child =>
                _details.add(child.details)
            }

            _detailsComputed = true
            _details
        }
    }

    func finish(): Unit {
        if (finished) { return }
        visitChildren { child =>
            match (child) {
                case c: ResultContainer<Result> => c.finish()
                case _ => ()
            }
        }
        finished = true
    }

    func visitAll<V>(visitor: (V) -> Unit): Unit where V <: Result {
        walk { node =>
            visitor(node as V ?? return)
        }
    }

    private func forAllStepsWithParent(visitor: (RunStepResult, TestCaseResult) -> Unit) {
        let currentCase: Box<?TestCaseResult> = Box(None)
        visitAll<Result> { r =>
            match (r) {
                case r: TestCaseResult => currentCase.value = r
                // case r: RunStepResult where !r.isPartOfTestCase() => visitor(r, None)
                case r: RunStepResult => visitor(r, currentCase.value.getOrThrow())
                case _ => ()
            }
        }
    }

    public func visitPassedTests(visitor: (TestCaseResult) -> Unit) {
        visitAll<TestCaseResult> { r =>
            if (r.hasFailures()) { return }
            visitor(r)
        }
    }

    public func visitPassedBenches(visitor: (BenchStepEntry) -> Unit) {
        forAllStepsWithParent { r, c =>
            if (r.statusCode().isFailure()) { return }

            if (let Bench(result) <- r.info) {
                if (let CaseStep(arg) <- r.kind) {
                    visitor(BenchStepEntry(result, arg, c.caseId, c.renderOptions.measurementInfo ?? TimeNow().info))
                }
            }
        }
    }

    public func visitFailedSteps(visitor: (RunStepResult, TestCaseResult) -> Unit) {
        forAllStepsWithParent { r, c =>
            if (!r.hasFailures()) { return }
            visitor(r, c)
        }
    }

    func serializeSubResults(): DataModelStruct{
        let serializablePieces = ArrayList<Result>()
        visitChildren{ s =>
            serializablePieces.add(s)
        }
        DataModelStruct()
            .add(field<ArrayList<Result>>("subResults", serializablePieces))
    }
}

struct BenchStepEntry {
    BenchStepEntry(
        let result: BenchmarkResult,
        let arg: ArgumentDescription,
        let caseId: TestCaseId,
        let measurementInfo: MeasurementInfo
    ) {
        result.calculate(measurementInfo)
    }
}

class RunStepResult <: Result {
    RunStepResult(
        let checksPassed: Int64,
        let startTimestamp: DateTime,
        let kind: StepKind,
        let info: StepInfo,
        let duration!: Duration = DateTime.now() - startTimestamp
    ) {}

    public prop details: Details {
        get() {
            let isAccountedStep = !kind.isClassLifecycle() || hasFailures()
            let d = Details(statusCode(), isAccountedStep)

            d.executedSteps = match (info) {
                case Test(x) => x
                case Bench(_) => 1
                case Failure(_) =>
                    d.failedSteps = 1
                    1
            }
            d.startTimestamp = startTimestamp
            d.duration = duration
            d
        }
    }

    static func simpleError(step: StepKind, check: CheckResult): RunStepResult {
        RunStepResult(0, DateTime.now(), step, Failure([check]), duration: Duration.Zero)
    }

    public func hasFailures(): Bool {
        statusCode().isFailure()
    }

    public prop checkResults: Array<CheckResult> {
        get() {
            match (info) {
                case Failure(c) => c
                case _ => []
            }
        }
    }

    public func statusCode(): TestCode {
        var code = match (kind) {
            case Skip => TestCode.SKIP
            case NoRun => TestCode.NORUN
            case _ => TestCode.PASS
        }

        let checkResults = match (info) {
            case Failure(_) where kind.isClassLifecycle() => return ERROR
            case Failure(c) => c
            case _ => return code
        }
        for (cr in checkResults) {
            let tc = match (cr) {
                case _: AssertExpectCheckResult => FAIL
                case _: MockFrameworkCheckResult => FAIL
                case _: TimeoutCheckResult => FAIL
                case _ => ERROR
            }

            code = code.mostSevereWith(tc)
            if (code == ERROR) { return code }
        }
        return code
    }
}

class TestCaseResult <: ResultContainer<RunStepResult> {
    TestCaseResult(
        let caseId: TestCaseId,
        let caseInfo: TestCaseReportInfo,
        let renderOptions: RenderOptions
    ) {}

    static func fromSingleStep(id: TestCaseId, info: TestCaseReportInfo, step: RunStepResult) {
        let caseResult = TestCaseResult(id, info, RenderOptions(None, None))
        caseResult.add(step)
        caseResult.finish()
        caseResult
    }

    private var _output: CapturedOutput = CapturedOutput()
    prop output: CapturedOutput {
        get() {
            _output
        }
    }

    public func addOutput(output: CapturedOutput): Unit {
        _output = output
    }

    // We can't store all successfull steps because there can be too much of them,
    // so we store only last successfull step but make it contain some info from previous steps
    // if in future we will want to count successfull steps as well we can do that here
    public func addSuccessfullStep(next: RunStepResult) {
        let first = subResults[0]
        let steps = if (let Test(steps) <- first.info) {
            steps
        } else {
            throw IllegalStateException("Unreachable")
        }
        let newResult = RunStepResult(
            first.checksPassed + next.checksPassed,
            first.startTimestamp,
            next.kind,
            Test(steps + 1),
            duration: first.duration + next.duration,
        )
        subResults[0] = newResult
    }

    public func hasFailures(): Bool {
        subResults |> any { s => s.hasFailures() }
    }
}

/**
 * Some information about test cases that should reach reporting facilities.
 */
struct TestCaseReportInfo {
    TestCaseReportInfo(let tags!: Array<String>) {}

    static func empty(): TestCaseReportInfo {
        TestCaseReportInfo(tags: [])
    }

    prop isEmpty: Bool {
        get() { this.tags.isEmpty() }
    }

    func merge(suiteInfo: TestSuiteReportInfo): TestCaseReportInfo {
        TestCaseReportInfo(tags: suiteInfo.tags.concat(this.tags))
    }
}

struct CapturedOutput {
    CapturedOutput(let stdout: String, let stderr: String) {}

    init() {
        this("", "")
    }

    init(stdout: Array<Byte>, stderr: Array<Byte>) {
        this(String.fromUtf8(stdout), String.fromUtf8(stderr))
    }

    func isEmpty(): Bool {
        stdout.isEmpty() && stderr.isEmpty()
    }
}

class TestSuiteResult <: ResultContainer<TestCaseResult> {
    TestSuiteResult(
        let suiteId: TestSuiteId,
        let suiteInfo: TestSuiteReportInfo
    ) {}

    func merge(other: TestSuiteResult) {
        this.subResults.add(all: other.subResults)
    }

    static func mergeAll(suiteResults: Iterable<TestSuiteResult>): Collection<TestSuiteResult> {
        let resultById = HashMap<TestSuiteId, TestSuiteResult>()
        for (suiteResult in suiteResults) {
            let id = suiteResult.suiteId
            if (resultById.contains(id)) {
                resultById[id].merge(suiteResult)
            } else {
                resultById[id] = suiteResult
            }
        }
        resultById.values()
    }
}

/**
 * Some information about test suites that should reach reporting facilities.
 */
struct TestSuiteReportInfo {
    TestSuiteReportInfo(let tags!: Array<String>) {}

    prop isEmpty: Bool {
        get() { this.tags.isEmpty() }
    }
}

enum GroupResultPart <: Result {
    | SuiteResult(TestSuiteResult)
    | ProcessError(TestProcessError)

    public func walk(visitor: (Result) -> Unit): Unit {
        match (this) {
            case SuiteResult(suiteResult) => 
                suiteResult.walk(visitor)
            case ProcessError(processError) =>
                processError.walk(visitor)
        }
    }

    public func visitAll<V>(visitor: (V) -> Unit): Unit where V <: Result {
        walk { grp =>
            visitor(grp as V ?? return)
        }
    }

    public prop details: Details {
        get() {
            match (this) {
                case SuiteResult(suiteResult) => 
                    suiteResult.details
                case ProcessError(processError) => 
                    processError.details
            }
        }
    }
}

class TestProcessError <: Result & Hashable {
    let _details = Details()

    TestProcessError(let code: Int64) {
        _details.add(Details(ERROR, true))
    }

    public func hashCode(): Int64 {
        var hasher = DefaultHasher()
        hasher.write(code)
        return hasher.finish()
    }

    public prop details: Details {
        get() {
            _details
        }
    }
}

class TestGroupResult <: ResultContainer<GroupResultPart> {
    TestGroupResult(
        let groupName: String,
        let nonTestOutputs: Array<NonTestOutputs>
    ) {}

    static func fromPackageSuites(packageName: String, suiteResults: Collection<TestSuiteResult>,
        processErrors: Array<TestProcessError>, nonTestOutputs: Array<NonTestOutputs>): TestGroupResult {
        let result = TestGroupResult(packageName, nonTestOutputs)
        for (suiteResult in suiteResults) {
            result.add(suiteResult)
        }
        for (err in processErrors) {
            result.add(err)
        }
        result.finish()
        result
    }

    prop hasProcessError: Bool {
        get() {
            for (value in this.subResults) {
                match (value) {
                    case ProcessError(_) => return true
                    case _ => continue
                }
            }

            return false
        }
    }

    func add(testSuite: Result): Unit {
        match (testSuite) {
            case suiteResultPart: TestSuiteResult => 
                let resultPart = GroupResultPart.SuiteResult(suiteResultPart)
                this.add(resultPart)
            case processErrorPart: TestProcessError => 
                let resultPart = GroupResultPart.ProcessError(processErrorPart)
                this.add(resultPart)
            case _ => throw Exception(
                "Internal error: expected TestSuiteResult or TestProcessError but got another type")
        }
    }
}

enum LStep <: ToString {
    | BeforeEach
    | BeforeAll
    | AfterAll
    | AfterEach

    operator func ==(other: LStep): Bool {
        match ((this, other)) {
            case (BeforeEach, BeforeEach) => true
            case (BeforeAll, BeforeAll) => true
            case (AfterAll, AfterAll) => true
            case (AfterEach, AfterEach) => true
            case _ => false
        }
    }

    public func toString() {
        match (this) {
            case BeforeEach => "BeforeEach"
            case BeforeAll => "BeforeAll"
            case AfterEach => "AfterEach"
            case AfterAll => "AfterAll"
        }
    }
}

enum StepKind {
    | Skip | NoRun
    | Lifecycle(LStep)
    | CaseStep(ArgumentDescription)
    | UserCode

    func isClassLifecycle(): Bool {
        match (this) {
            case Lifecycle(BeforeAll) | Lifecycle(AfterAll) => true
            case _ => false
        }
    }
}

struct ArgumentDescription {
    ArgumentDescription(
        let textDescription: ?PrettyText,
        let stepIndex: Int64,
        let reductionIndex: Int64,
        let randomSeed: ?Int64
    ) {}

    func intoErrorMessage(): ?PrettyText {
        let argsText = this.textDescription ?? return None
        let messageBuilder = PrettyText("After ${this.stepIndex + 1} generation steps")
        if (this.reductionIndex != 0) {
            messageBuilder.appendLine(" and ${this.reductionIndex} reduction steps:")
        } else {
            messageBuilder.appendLine(":")
        }

        messageBuilder.indent {
            messageBuilder.append(argsText)
        }

        match (this.randomSeed) {
            case Some(seed) => messageBuilder.appendLine("with randomSeed = ${seed}")
            case _ => ()
        }

        messageBuilder
    }
}

enum StepInfo {
    | Test( /*aggregatedSteps:*/ Int64)
    | Bench(BenchmarkResult)
    | Failure(Array<CheckResult>)
}

// render options are an extract from configuration that affects result report
// we cannot serialize the whole configuration
// but all render options must be serializable (report itself can be in the other process)
struct RenderOptions {
    RenderOptions(
        let baselineString: ?String,
        let measurementInfo: ?MeasurementInfo
    ) {}

    static func fromConfiguration(configuration: Configuration) {
        RenderOptions(
            configuration.get(KeyBaseline.baseline),
            configuration.get(KeyMeasurementInfo.measurementInfo)
        )
    }
}