Editor/Dock/SoloTestCard.cs
using System;
using System.Linq;
using Editor;
using Sandbox;

namespace TeamCreate;

/// <summary>
/// The "Try it alone" card. In a real project it offers one button, which copies the project into a disposable folder, opens that copy in a second
/// editor window, and runs the solo test there by itself. In a disposable copy it shows the test's checklist, live. It never runs the test in a real
/// project, because the test relies on an identity the host only accepts inside a copy.
/// </summary>
public sealed class SoloTestCard : DockCard
{
    private readonly Button run;

    private readonly Button stop;

    private readonly Button openReport;

    private readonly Label notice;

    private readonly Label summary;

    private readonly Widget list;

    private string lastSignature;

    private string status;

    private bool statusIsError;

    protected override bool FieldsAssigned => run != null && stop != null && notice != null && summary != null && list != null && openReport != null;

    public SoloTestCard(Widget parent)
        : base(parent, "Try it alone")
    {
        notice = Layout.Add(new Label("", this)
        {
            WordWrap = true,
            Color = DockTheme.MutedColor
        });
        run = Layout.Add(new Button.Primary("Run solo test", this));
        run.ToolTip = "Copies this project to a disposable folder, opens the copy in a second editor window, and runs a test there in which a scripted collaborator joins, edits and leaves. Your project is never changed.";
        run.Clicked = () => DockCard.Run(RunClicked);
        stop = Layout.Add(new Button("Stop the test", this));
        stop.ToolTip = "Ends the test: the collaborator leaves, the object it made is removed, and the session ends.";
        stop.Clicked = () => DockCard.Run(SoloTestRunner.Stop);
        summary = Layout.Add(new Label("", this)
        {
            WordWrap = true,
            Color = DockTheme.MutedColor
        });
        list = Layout.Add(new Widget(this));
        list.Layout = Layout.Column();
        list.Layout.Spacing = 2f;
        openReport = Layout.Add(new Button("Open the last report", this));
        openReport.ToolTip = "The last run's results as a file, one line per check.";
        openReport.Clicked = () => DockCard.Run(OpenReport);
        stop.Visible = false;
        openReport.Visible = false;
        list.Visible = false;
    }

    private static string ProjectRoot()
    {
        try
        {
            return SceneAdapter.Root;
        }
        catch (Exception)
        {
            return null;
        }
    }

    private void RunClicked()
    {
        status = null;
        statusIsError = false;
        if (SoloTestRunner.IsSoloCopy)
        {
            SoloTestRunner.Start();
            return;
        }
        LaunchCopy();
    }

    private void LaunchCopy()
    {
        string root = ProjectRoot();
        CollaborationSession session = CollaborationSession.Current;
        if (session.Busy)
        {
            Say("Leave the current collaboration session before running the solo test.", true);
            return;
        }
        if (SceneEditorSession.All.Any((SceneEditorSession x) => !(x is GameEditorSession) && x.HasUnsavedChanges))
        {
            Say("Save your scene first, so the copy is made from what you see.", true);
            return;
        }
        string memory = SoloCopy.MemoryAdvice(SoloTestLauncher.AvailableMemoryBytes());
        if (memory != null)
        {
            Say(memory, true);
            return;
        }
        string existing = SoloCopy.CopyRoot(root);
        if (SoloTestLauncher.IsRunning(existing))
        {
            Say("The solo test window is already open. Switch to it.", false);
            return;
        }
        Say("Copying the project…", false);
        SoloCopyResult copy = SoloCopy.Build(root, autorun: true);
        if (!copy.Ok)
        {
            Say(copy.Error, true);
            return;
        }
        string error = SoloTestLauncher.Launch(copy.Path);
        if (error != null)
        {
            Say(error, true);
            return;
        }
        Say("Opened a solo test window (" + copy.Files + " files copied). It runs the test by itself in about a minute; watch that window.", false);
    }

    private void OpenReport()
    {
        string root = ProjectRoot();
        string path = System.IO.Path.Combine(SoloCopy.CopyRoot(root), ".collaboration", SoloTestRunner.ReportFile);
        if (!System.IO.File.Exists(path))
        {
            path = System.IO.Path.Combine(root, ".collaboration", SoloTestRunner.ReportFile);
        }
        if (System.IO.File.Exists(path))
        {
            EditorUtility.OpenFile(path);
        }
    }

    private void Say(string text, bool error)
    {
        status = text;
        statusIsError = error;
    }

    public void Refresh()
    {
        string root = ProjectRoot();
        bool inCopy = SoloTestRunner.IsSoloCopy;
        bool active = SoloTestRunner.Active;
        string lastRun = inCopy ? SoloTestRunner.LastRunSummary(root) : SoloTestRunner.LastRunSummary(SoloCopy.CopyRoot(root ?? string.Empty));
        string signature = inCopy + "|" + active + "|" + SoloTestRunner.Headline + "|" + status + "|" + statusIsError + "|" + lastRun + "|" + string.Join(";", SoloTestRunner.Steps.Select((SoloStep s) => s.State + s.Detail));
        if (string.Equals(signature, lastSignature, StringComparison.Ordinal))
        {
            return;
        }
        lastSignature = signature;

        string intro;
        if (inCopy)
        {
            intro = active
                ? "Running in this solo test copy. The blue sphere is a scripted collaborator."
                : "This is a disposable solo test copy. Nothing here is your real project.";
        }
        else
        {
            intro = "See two people editing together with just this editor. The test runs in a disposable copy of the project, so your scenes are never touched.";
        }
        DockCard.SetText(notice, status ?? intro);
        notice.Color = statusIsError ? DockTheme.BlockedColor : DockTheme.MutedColor;
        DockTheme.SetText(run, active ? "Running…" : (inCopy ? "Run the test again" : "Run solo test"));
        run.Enabled = !active;
        stop.Visible = active;
        bool haveList = SoloTestRunner.Steps.Count > 0;
        list.Visible = haveList;
        DockCard.SetText(summary, haveList ? SoloTestRunner.Headline : (lastRun != null ? "Last run: " + lastRun : string.Empty));
        summary.Visible = haveList || lastRun != null;
        openReport.Visible = !inCopy && lastRun != null;
        RebuildList();
    }

    private void RebuildList()
    {
        list.Layout.Clear(deleteWidgets: true);
        foreach (SoloStep step in SoloTestRunner.Steps)
        {
            Color color = step.State == SoloStepState.Passed ? DockTheme.OkColor : step.State == SoloStepState.Failed ? DockTheme.BlockedColor : step.State == SoloStepState.Running ? Theme.Text : DockTheme.MutedColor;
            string text = SoloReport.Symbol(step.State) + "  " + step.Title;
            if (!string.IsNullOrWhiteSpace(step.Detail) && (step.State == SoloStepState.Failed || step.State == SoloStepState.Running || step.State == SoloStepState.Skipped))
            {
                text += "\n      " + step.Detail;
            }
            list.Layout.Add(new Label(text, list)
            {
                WordWrap = true,
                Color = color,
                ToolTip = step.Detail ?? step.Title
            });
        }
    }
}