//
//  RealShellTest.swift
//  PHP Monitor
//
//  Created by Nico Verbruggen on 28/09/2022.
//  Copyright © 2025 Nico Verbruggen. All rights reserved.
//

import Testing
import Foundation

@Suite(.serialized)
struct RealShellTest {
    var container: Container

    init() async throws {
        // Reset to the default shell
        container = Container.real()
    }

    @Test func system_shell_is_default() async {
        #expect(container.shell is RealShell)

        let output = await container.shell.pipe("php -v")

        #expect(output.out.contains("Copyright (c) The PHP Group"))
    }

    @Test func system_shell_can_be_used_synchronously() {
        #expect(container.shell is RealShell)

        let output = container.shell.sync("php -v")

        #expect(output.out.contains("Copyright (c) The PHP Group"))
    }

    @Test func system_shell_has_path() {
        let systemShell = container.shell as! RealShell

        #expect(systemShell.PATH.contains(":/usr/local/bin"))
        #expect(systemShell.PATH.contains(":/usr/bin"))
    }

    @Test func system_shell_can_buffer_output() async {
        var bits: [String] = []

        let (_, shellOutput) = try! await container.shell.attach(
            "php -r \"echo 'Hello world' . PHP_EOL; usleep(500); echo 'Goodbye world';\"",
            didReceiveOutput: { incoming, _ in
                bits.append(incoming)
            },
            withTimeout: 2.0
        )

        #expect("Hello world\nGoodbye world" == shellOutput.out)
    }

    @Test func system_shell_can_timeout_and_throw_error() async {
        await #expect(throws: ShellError.timedOut) {
            try await container.shell.attach(
                "php -r \"sleep(1);\"",
                didReceiveOutput: { _, _ in },
                withTimeout: .seconds(0.1)
            )
        }
    }

    @Test func can_run_multiple_shell_commands_in_parallel() async throws {
        let start = ContinuousClock.now

        await withTaskGroup(of: Void.self) { group in
            group.addTask { await container.shell.quiet("php -r \"usleep(700000);\"") }
            group.addTask { await container.shell.quiet("php -r \"usleep(700000);\"") }
            group.addTask { await container.shell.quiet("php -r \"usleep(700000);\"") }
        }

        let duration = start.duration(to: .now)
        #expect(duration < .milliseconds(2000)) // Should complete in ~700ms if parallel
    }

    /**
     This test verifies that concurrent writes to `output.out` and `output.err`
     from multiple readability handlers don't cause data races or crashes,
     and that the output is correct (for both stdout and stderr output).

     When Thread Sanitizer is enabled, this will also check if any potential
     data races occur. None should, at this point. You can enable the
     Thread Sanitizer by editing the Test Plan's Configurations.

     This test was added specifically to diagnose and fix one such reported
     data race, which was fixed by adding a serial queue to the shell's
     `attach()` method, since the readability handlers actually run
     on separate threads.
     */
    @Test func attach_handles_concurrent_stdout_stderr_writes_safely() async throws {
        // Create a PHP script that will output lots of text to STDOUT and STDERR.
        let phpScript = "php -r 'for ($i = 1; $i <= 500; $i++) { fwrite(STDOUT, \"stdout-$i\" . PHP_EOL); fwrite(STDERR, \"stderr-$i\" . PHP_EOL); flush(); }'"

        // Keep track of the total chunk count
        var receivedChunks = 0

        // We will now test the attach method
        let (_, shellOutput) = try await container.shell.attach(
            phpScript,
            didReceiveOutput: { _, _ in
                receivedChunks += 1
            },
            withTimeout: 5.0
        )

        // Verify all output was captured without corruption
        let stdoutLines = shellOutput.out
            .components(separatedBy: "\n")
            .filter { !$0.isEmpty }
        let stderrLines = shellOutput.err
            .components(separatedBy: "\n")
            .filter { !$0.isEmpty }

        #expect(stdoutLines.count == 500)
        #expect(stderrLines.count == 500)

        // Verify content integrity
        for i in 1...200 {
            #expect(stdoutLines.contains("stdout-\(i)"))
            #expect(stderrLines.contains("stderr-\(i)"))
        }
    }
}
