using ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.ScriptAnalysis;

/// <summary>
/// Regression tests for the <c>SandboxConsoleCapture</c> writer that the Test Run
/// sandbox installs on <c>Console.Out</c>/<c>Console.Error</c>. CentralUI-030
/// surfaced an intra-script concurrency hazard: a sandboxed script can fan out
/// work with <c>Task.WhenAll</c> / <c>Task.Run</c> and every child task inherits
/// the capture <c>StringWriter</c> via <c>AsyncLocal</c>; <c>StringWriter</c> is
/// not thread-safe, so concurrent writes could corrupt the buffer. These tests
/// drive the writer the same way Roslyn-hosted user code does.
/// </summary>
public class SandboxConsoleCaptureTests
{
    /// <summary>
    /// CentralUI-030: a capture scope shared across <c>Task.WhenAll</c> child
    /// tasks must serialise writes so the resulting transcript contains exactly
    /// the expected number of lines without character-level interleaving.
    /// </summary>
    [Fact]
    public async Task BeginCapture_ConcurrentWritesFromTasks_DoNotCorruptBuffer()
    {
        // The static install routes Console.Out through the singleton sandbox
        // capture writer for the test process — this is idempotent and matches
        // the way ScriptAnalysisService bootstraps the sandbox in production.
        var (capture, _) = SandboxConsoleCapture.Install();

        var buffer = new StringWriter();
        const int taskCount = 32;
        const int linesPerTask = 50;
        const int expectedLines = taskCount * linesPerTask;

        using (capture.BeginCapture(buffer))
        {
            // AsyncLocal flows the capture scope into each Task.Run, mirroring
            // a sandboxed script doing `await Task.WhenAll(...)` over Tasks
            // that each `Console.WriteLine`.
            var tasks = Enumerable.Range(0, taskCount).Select(i => Task.Run(() =>
            {
                for (var j = 0; j < linesPerTask; j++)
                {
                    Console.WriteLine($"task-{i}-line-{j}");
                }
            }));
            await Task.WhenAll(tasks);
        }

        var captured = buffer.ToString();
        // Without the lock, concurrent StringWriter.WriteLine can drop or
        // interleave characters and produce malformed lines / a wrong count.
        // We assert the exact line count and that every emitted token is
        // present on a line of its own — both fail under the unprotected
        // implementation.
        var lines = captured.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
        Assert.Equal(expectedLines, lines.Length);

        for (var i = 0; i < taskCount; i++)
        {
            for (var j = 0; j < linesPerTask; j++)
            {
                Assert.Contains($"task-{i}-line-{j}", lines);
            }
        }
    }

    /// <summary>
    /// Sanity check: the most basic capture happy-path still works after the
    /// CentralUI-030 lock was introduced.
    /// </summary>
    [Fact]
    public void BeginCapture_SingleThreadedWrites_AreCaptured()
    {
        var (capture, _) = SandboxConsoleCapture.Install();
        var buffer = new StringWriter();

        using (capture.BeginCapture(buffer))
        {
            Console.WriteLine("hello");
            Console.Write("world");
        }

        Assert.Contains("hello", buffer.ToString());
        Assert.Contains("world", buffer.ToString());
    }
}
