using Godosa.Core.Runs;
using Godosa.Core.Runs.Scripts;
using Xunit;
using static Godosa.Core.Tests.Runs.Scripts.ScriptRunnerTests;
namespace Godosa.Core.Tests.Runs.Scripts;
/// Final-review findings (lib core): each test failed before its fix.
public sealed class ReviewFixTests : IDisposable
{
private readonly string _dir = Path.Combine(AppContext.BaseDirectory, "rs-tmp", Guid.NewGuid().ToString("N"));
public void Dispose()
{
if (Directory.Exists(_dir))
Directory.Delete(_dir, recursive: true);
}
private const string Script = "format 1\nmode player\nkey A\ncheckpoint one\nkey B\ncheckpoint two\nkey C";
[Fact]
public void NoArgVerb_AndEmptySet_Run()
{
var g = new FakeGame();
g.ActionTable["close-panel"] = new FakeAction(VerbKind.Player, _ => new ActionStart(Status: StepStatus.Done), min: 0, max: 0);
var (r, output) = Run("format 1\nmode player\nset x =\nclose-panel\nlog \"[${x}]\"", g);
Assert.Equal(RunOutcome.Passed, r.Outcome);
Assert.Contains("[]", output);
}
// A probe value (dialog text) with quotes inside a quoted argument stays text: the line is split into tokens first.
[Fact]
public void QuotedArgument_ValueWithQuotes_StaysOneToken()
{
var (r, output) = Run("format 1\nmode player\nset x = \"\\\"Hi,\\\" he said\"\nlog \"R: ${x}\"", new FakeGame());
Assert.Equal(RunOutcome.Passed, r.Outcome);
Assert.Contains("R: \"Hi,\" he said", output);
}
[Fact]
public void SubstitutedValues_AreNotRescanned()
{
var values = new Dictionary { ["d"] = "$", ["e"] = "{x}", ["raw"] = "${y}" };
Assert.Equal("${x}", VarExpander.Expand("${d}${e}", n => values.GetValueOrDefault(n), null));
Assert.Equal("say ${y}!", VarExpander.Expand("say ${raw}!", n => values.GetValueOrDefault(n), null));
}
[Theory]
[InlineData("log Done:", "log", "Done:")]
[InlineData("type name:", "type", "name:")]
public void LineEndingInColon_IsAVerbUnlessABlockKeyword(string line, string verb, string args)
{
var f = ScriptParser.Parse("main.steps", $"format 1\nmode debug\n{line}");
Assert.Empty(f.Errors);
var v = Assert.IsType(f.Body.Single());
Assert.Equal((verb, args), (v.Verb, v.Args));
}
[Fact]
public void ThrowingInputRouter_FailsTheRun()
{
var g = new FakeGame { OnSend = _ => throw new InvalidOperationException("router down") };
var (r, output) = Run("format 1\nmode player\nkey R", g);
Assert.Equal(RunOutcome.Failed, r.Outcome);
Assert.Contains(output, l => l == "FAIL main.steps:3 key R: InvalidOperationException: router down");
}
[Fact]
public void CorruptCacheEntry_IsAMiss()
{
Run(Script, new FakeGame(), new ScriptRunOptions { CacheDir = _dir });
foreach (var f in Directory.GetFiles(_dir, "*.snap"))
File.WriteAllBytes(f, [.. "GRS1"u8, 3, 0, 0, 0, (byte)'{', (byte)'x', (byte)'!']);
var (r, output) = Run(Script, new FakeGame(), new ScriptRunOptions { CacheDir = _dir });
Assert.Equal(RunOutcome.Passed, r.Outcome);
Assert.DoesNotContain(output, l => l.StartsWith("resumed"));
}
// A game state the snapshot can't hold (an open dialog) → checkpoint not cached, run unharmed.
[Fact]
public void RefusedSnapshot_CheckpointNotCached()
{
var g = new FakeGame { Refusal = "dialog open" };
var output = new List();
var runner = new ScriptRunner(ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary
{
["main.steps"] = Script,
}), new Dictionary()), g, new ScriptRunOptions { CacheDir = _dir }, output.Add);
Assert.Equal(RunOutcome.Passed, runner.Run(1000).Outcome);
Assert.Equal(["checkpoint one not cached: dialog open", "checkpoint two not cached: dialog open"], runner.Report!.Warnings);
Assert.False(Directory.Exists(_dir) && Directory.GetFiles(_dir, "*.snap", SearchOption.AllDirectories).Length > 0);
}
[Fact]
public void FailingRestore_FailsTheRun()
{
Run(Script, new FakeGame(), new ScriptRunOptions { CacheDir = _dir });
var (r, output) = Run(Script, new FakeGame { RestoreThrows = true }, new ScriptRunOptions { CacheDir = _dir });
Assert.Equal((RunOutcome.Failed, 1), (r.Outcome, r.ExitCode));
Assert.Contains(output, l => l.Contains("IOException: restore broke"));
}
[Fact]
public void UnwritableCacheAndReportDirs_WarnInsteadOfThrowing()
{
Directory.CreateDirectory(_dir);
var file = Path.Combine(_dir, "file");
File.WriteAllText(file, "x");
var g = new FakeGame();
g.ProbeFns["no"] = _ => RunValue.Of(false);
var output = new List();
var runner = new ScriptRunner(ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary
{
["main.steps"] = "format 1\nmode player\ncheckpoint one\nexpect no",
}), new Dictionary()), g, new ScriptRunOptions { CacheDir = Path.Combine(file, "cache"), ReportDir = Path.Combine(file, "report") }, output.Add);
var r = runner.Run(1000);
Assert.Equal(RunOutcome.Failed, r.Outcome);
Assert.Contains(runner.Report!.Warnings, w => w.StartsWith("checkpoint cache not written:"));
Assert.Contains(output, l => l.StartsWith("warning: report not written:"));
}
[Fact]
public void Until_UnknownName_Invalid()
{
var (r, output) = Run(Script, new FakeGame(), new ScriptRunOptions { Until = "nowhere" });
Assert.Equal((RunOutcome.Invalid, 2), (r.Outcome, r.ExitCode));
Assert.Contains("unknown label or checkpoint nowhere", output);
}
[Fact]
public void Until_ResumesOnlyBeforeIt()
{
Run(Script, new FakeGame(), new ScriptRunOptions { CacheDir = _dir }); // caches one and two
var output = new List();
var r = new ScriptRunner(ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary { ["main.steps"] = Script }),
new Dictionary()), new FakeGame(), new ScriptRunOptions { CacheDir = _dir, Until = "two" }, output.Add).Run(1000);
Assert.Contains("resumed at one (cache) f=2", output);
Assert.Equal((RunOutcome.Stopped, 4L), (r.Outcome, r.Frames));
}
[Fact]
public void ScriptSeedHeader_SeedsTheGame_CommandLineWins()
{
var g = new FakeGame();
var output = new List();
var runner = new ScriptRunner(ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary { ["main.steps"] = "format 1\nmode player\nseed 7\nkey A" }),
new Dictionary()), g, new ScriptRunOptions(), output.Add);
runner.Run(1000);
Assert.Equal(["seed 7"], g.RngImpl!.Calls);
Assert.Equal(7UL, runner.Report!.Seed);
var g2 = new FakeGame();
var runner2 = new ScriptRunner(ScriptLoader.Load("main.steps", new MemoryScriptSource(new Dictionary { ["main.steps"] = "format 1\nmode player\nseed 7\nkey A" }),
new Dictionary()), g2, new ScriptRunOptions { Seed = 9 }, output.Add);
runner2.Run(1000);
Assert.Equal(["seed 9"], g2.RngImpl!.Calls);
}
}