unit test_CmdLineBuilder;

{$mode objfpc}{$H+}

/// <summary>
/// test_CmdLineBuilder — FPCUnit tests for uHCLCmdLineBuilder.
///
/// Covers every flag group, edge cases (locale, precedence, accumulator
/// short-circuit), and a full-realistic combination test.
/// </summary>

interface

uses
  Classes, SysUtils,
  fpcunit, testregistry,
  uHCLCmdLineBuilder;

type
  /// <summary>
  /// FPCUnit test suite for <see cref="THCLCmdLineBuilder"/>.
  /// </summary>
  TCmdLineBuilderTests = class(TTestCase)
  private
    FBuilder: IHCLCmdLineBuilder;

    /// <summary>
    /// Returns the path to the system temporary directory (always exists).
    /// </summary>
    function TempDir: String;

    /// <summary>
    /// Returns the path of this test executable (always exists as a file).
    /// </summary>
    function ExistingFile: String;

    /// <summary>
    /// Returns a path that is guaranteed NOT to exist.
    /// </summary>
    function NonExistentPath: String;

    /// <summary>
    /// Builds a command line from <paramref name="AInputs"/> using FBuilder
    /// and returns the result string.
    /// </summary>
    function Build(const AInputs: THCGECmdLineInputs): String;

  protected
    procedure SetUp; override;
    procedure TearDown; override;

  published
    // --- Basic -------------------------------------------------------
    procedure TestBuildEmpty;

    // --- -f (log file) -----------------------------------------------
    procedure TestLogFile;
    procedure TestLogFileInvalidDir;

    // --- -l (logging) ------------------------------------------------
    procedure TestLogAll;
    procedure TestLogIndividual_InitAnnex;
    procedure TestLogIndividual_Multiple;
    procedure TestLogAllOverridesIndividual;

    // --- -I (debug) --------------------------------------------------
    procedure TestDebugI32501;
    procedure TestDebugDisabled;

    // --- -h (window hide) --------------------------------------------
    procedure TestWindowHideGroup;
    procedure TestWindowHideMultiple;
    procedure TestWindowHideDisabled;

    // --- Simple switches ---------------------------------------------
    procedure TestIgnoreReg;
    procedure TestAutoPlay;
    procedure TestSubAI;
    procedure TestEff115;

    // --- -t (save interval) ------------------------------------------
    procedure TestSavInterval_Valid;
    procedure TestSavInterval_CommaSeparator;
    procedure TestSavInterval_Invalid;

    // --- -s (iterative save) -----------------------------------------
    procedure TestIterSave;

    // --- -S/-k (iterative save GT) -----------------------------------
    procedure TestIterSaveGT;
    procedure TestIterSaveGT_WithFilename;

    // --- -m (message log) --------------------------------------------
    procedure TestMessageLog;

    // --- -b (battle set) ---------------------------------------------
    procedure TestBSByNum_Index0;
    procedure TestBSByNum_Index4;
    procedure TestBSByNum_Index30;
    procedure TestBSByNum_Index31;
    procedure TestBSByFile;
    procedure TestBSNumPrecedence;

    // --- -c (scenario) -----------------------------------------------
    procedure TestScenByNum;
    procedure TestScenByNum_Max;
    procedure TestScenByFile;

    // --- -v (save file) ----------------------------------------------
    procedure TestSavFile;

    // --- -d/-r (CD databases) ----------------------------------------
    procedure TestCDBresFN;
    procedure TestCDBrsrFN;

    // --- -g (game options) -------------------------------------------
    procedure TestGameOpts_NewGE;
    procedure TestGameOpts_OldGE;
    procedure TestGameOpts_MixedIndices;

    // --- -p (CPU affinity) -------------------------------------------
    procedure TestAffinity_Index0;
    procedure TestAffinity_Index1;
    procedure TestAffinity_Index2;
    procedure TestAffinity_Index3;

    // --- -u (process priority) ---------------------------------------
    procedure TestPriority_Index0;
    procedure TestPriority_Index2;
    procedure TestPriority_Index4;

    // --- Gap coverage ------------------------------------------------

    // -f edge cases
    procedure TestLogFile_EmptyPath;

    // -l master switch off
    procedure TestLog_MasterSwitchOff;

    // Individual -l flags (each token)
    procedure TestLogIndividual_AcAI;
    procedure TestLogIndividual_SubAI;
    procedure TestLogIndividual_EmconAI;
    procedure TestLogIndividual_Sonar;
    procedure TestLogIndividual_Refuel;
    procedure TestLogIndividual_Mission;
    procedure TestLogIndividual_Intercept;
    procedure TestLogIndividual_ResFile;
    procedure TestLogIndividual_Load;
    procedure TestLogIndividual_Radar;
    procedure TestLogIndividual_ECM;
    procedure TestLogIndividual_Missile;
    procedure TestLogIndividual_AreaDef;
    procedure TestLogIndividual_Perf;
    procedure TestLogIndividual_BadDB;
    procedure TestLogIndividual_Graphic;
    procedure TestLogIndividual_Unit;

    // -h all 5 sub-flags at once
    procedure TestWindowHideAllFive;

    // -S with zero interval and saves
    procedure TestIterSaveGT_ZeroValues;

    // -S without -k (IterSaveGTFNEnabled=False)
    procedure TestIterSaveGT_NoFilename;

    // -b out of range
    procedure TestBSByNum_OutOfRange_High;
    procedure TestBSByNum_OutOfRange_Negative;

    // -c with ScenNumIndex=-1
    procedure TestScenByNum_NegativeIndex;

    // -v/-d/-r with non-existent file
    procedure TestSavFile_NonExistent;
    procedure TestCDBresFN_NonExistent;
    procedure TestCDBrsrFN_NonExistent;

    // -p out of range
    procedure TestAffinity_OutOfRange_High;
    procedure TestAffinity_OutOfRange_Negative;

    // -u with PriorityIndex=-1
    procedure TestPriority_NegativeIndex;

    // -g all 9 digits non-zero
    procedure TestGameOpts_AllNineDigits;

    // --- Integration -------------------------------------------------
    procedure TestFlagOrdering;
    procedure TestFullRealistic;

    // --- Scenario Editor (BuildCommandLineSE) -------------------------
    /// <summary>No options: an empty SE line.</summary>
    procedure TestSE_Empty;
    /// <summary>GE-only options (save interval -t, -a, -i, -h, -I, -f, -g, -c, -v, a -b file) never reach the SE.</summary>
    procedure TestSE_GEOnlyOptionsLeftOut;
    /// <summary>Logging passes through, except -l perf; the GE line still has perf.</summary>
    procedure TestSE_LoggingWithoutPerf;
    /// <summary>-l all passes through as is.</summary>
    procedure TestSE_LogAll;
    /// <summary>A battleset number, the database files, in order: -l, -b, -d, -r.</summary>
    procedure TestSE_BattleSetAndDatabases;
    /// <summary>Database files that do not exist are not passed.</summary>
    procedure TestSE_MissingDatabaseFilesLeftOut;
    /// <summary>An explicit battleset number wins over the picker and a file, for both GE and SE, even one the picker lacks.</summary>
    procedure TestBSNumberExplicit;
  end;

// ---------------------------------------------------------------------------
// Helper — return an inputs record with all safe zero-value defaults
// ---------------------------------------------------------------------------

/// <summary>
/// Returns a <see cref="THCGECmdLineInputs"/> record with all booleans False,
/// all strings empty, and all index fields set to their "not selected" defaults.
/// Use this as a baseline in tests, then override only the fields under test.
/// </summary>
/// <returns>Zero-value inputs record.</returns>
function EmptyInputs: THCGECmdLineInputs;

implementation

// ---------------------------------------------------------------------------
// EmptyInputs
// ---------------------------------------------------------------------------

function EmptyInputs: THCGECmdLineInputs;
begin
  Result := DefaultCmdLineInputs;
end;

// ---------------------------------------------------------------------------
// TCmdLineBuilderTests — setup/teardown
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.SetUp;
begin
  FBuilder := THCLCmdLineBuilder.Create;
end;

procedure TCmdLineBuilderTests.TearDown;
begin
  FBuilder := nil;
end;

// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------

function TCmdLineBuilderTests.TempDir: String;
begin
  Result := IncludeTrailingPathDelimiter(GetTempDir);
end;

function TCmdLineBuilderTests.ExistingFile: String;
begin
  Result := ParamStr(0);
end;

function TCmdLineBuilderTests.NonExistentPath: String;
begin
  // Path whose DIRECTORY does not exist — used to test -f suppression.
  Result := TempDir + 'hcl_nonexistent_dir_xyz9999' + PathDelim + 'logfile.log';
end;

function TCmdLineBuilderTests.Build(const AInputs: THCGECmdLineInputs): String;
begin
  Result := FBuilder.BuildCommandLine(AInputs);
end;

// ---------------------------------------------------------------------------
// Tests — Basic
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.TestBuildEmpty;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  AssertEquals('Empty inputs must produce empty string', '', Build(Inp));
end;

// ---------------------------------------------------------------------------
// Tests — -f (log file)
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.TestLogFile;
var
  Inp: THCGECmdLineInputs;
  Path: String;
  Res: String;
begin
  // Use the temp dir as a known-existing directory; append a filename.
  Path := TempDir + 'test_hcl.log';
  Inp := EmptyInputs;
  Inp.LogFN     := True;
  Inp.LogFNPath := Path;
  Res := Build(Inp);
  AssertTrue('Result must contain -f flag', Pos('-f "', Res) > 0);
  AssertTrue('Result must contain the log path', Pos(Path, Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogFileInvalidDir;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.LogFN     := True;
  Inp.LogFNPath := NonExistentPath;
  AssertEquals('Nonexistent dir must suppress -f', '', Build(Inp));
end;

// ---------------------------------------------------------------------------
// Tests — -l (logging)
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.TestLogAll;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogAll         := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l all', Pos('-l all', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogIndividual_InitAnnex;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogInitAnnex   := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l init_annex', Pos('-l init_annex', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogIndividual_Multiple;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogRadar       := True;
  Inp.LogECM         := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l radar', Pos('-l radar', Res) > 0);
  AssertTrue('Must contain -l ecm',   Pos('-l ecm',   Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogAllOverridesIndividual;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogAll         := True;
  Inp.LogRadar       := True;  // Should be suppressed by LogAll
  Res := Build(Inp);
  AssertTrue('Must contain -l all',      Pos('-l all',   Res) > 0);
  AssertEquals('Must not contain -l radar', 0, Pos('-l radar', Res));
end;

// ---------------------------------------------------------------------------
// Tests — -I (debug)
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.TestDebugI32501;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.DebugEnabled := True;
  Inp.DebugI32501  := True;
  Res := Build(Inp);
  AssertTrue('Must contain -I I32501', Pos('-I I32501', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestDebugDisabled;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.DebugEnabled := False;
  Inp.DebugI32501  := True;  // master switch is off
  AssertEquals('DebugEnabled=False must suppress -I', '', Build(Inp));
end;

// ---------------------------------------------------------------------------
// Tests — -h (window hide)
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.TestWindowHideGroup;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.WindowHideEnabled := True;
  Inp.WHGroup           := True;
  Res := Build(Inp);
  AssertTrue('Must contain -h group', Pos('-h group', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestWindowHideMultiple;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.WindowHideEnabled := True;
  Inp.WHGroup           := True;
  Inp.WHUnit            := True;
  Inp.WHStratMap        := True;
  Res := Build(Inp);
  AssertTrue('Must contain -h group',       Pos('-h group',       Res) > 0);
  AssertTrue('Must contain -h unit',        Pos('-h unit',        Res) > 0);
  AssertTrue('Must contain -h strategicmap',Pos('-h strategicmap',Res) > 0);
end;

procedure TCmdLineBuilderTests.TestWindowHideDisabled;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.WindowHideEnabled := False;
  Inp.WHGroup           := True;  // master switch off
  AssertEquals('WindowHideEnabled=False must suppress -h', '', Build(Inp));
end;

// ---------------------------------------------------------------------------
// Tests — Simple switches
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.TestIgnoreReg;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.IgnoreReg := True;
  AssertEquals('IgnoreReg must emit -i', '-i', Build(Inp));
end;

procedure TCmdLineBuilderTests.TestAutoPlay;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.AutoPlay := True;
  AssertEquals('AutoPlay must emit -a', '-a', Build(Inp));
end;

procedure TCmdLineBuilderTests.TestSubAI;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.SubAIDisable := True;
  AssertEquals('SubAIDisable must emit -x', '-x', Build(Inp));
end;

procedure TCmdLineBuilderTests.TestEff115;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.Eff115Old := True;
  AssertEquals('Eff115Old must emit -e', '-e', Build(Inp));
end;

// ---------------------------------------------------------------------------
// Tests — -t (save interval)
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.TestSavInterval_Valid;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.SavIntEnabled := True;
  Inp.SavIntText    := '1.5';
  Res := Build(Inp);
  AssertTrue('Must contain -t 1.5', Pos('-t 1.5', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestSavInterval_CommaSeparator;
var
  Inp: THCGECmdLineInputs;
begin
  // Comma-as-decimal is common on many European locales; the builder must
  // reject it because the GE expects a period.
  Inp := EmptyInputs;
  Inp.SavIntEnabled := True;
  Inp.SavIntText    := '1,5';
  AssertEquals('Comma separator must suppress -t', '', Build(Inp));
end;

procedure TCmdLineBuilderTests.TestSavInterval_Invalid;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.SavIntEnabled := True;
  Inp.SavIntText    := 'abc';
  AssertEquals('Non-numeric text must suppress -t', '', Build(Inp));
end;

// ---------------------------------------------------------------------------
// Tests — -s (iterative save)
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.TestIterSave;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.IterSave := True;
  AssertEquals('IterSave must emit -s', '-s', Build(Inp));
end;

// ---------------------------------------------------------------------------
// Tests — -S/-k (iterative save GT)
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.TestIterSaveGT;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.IterSaveGT         := True;
  Inp.IterSaveGTInterval := 100;
  Inp.IterSaveGTSaves    := 5;
  Res := Build(Inp);
  // Interval is 5 digits, saves are 4 digits: -S001000005
  AssertTrue('Must contain -S001000005', Pos('-S001000005', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestIterSaveGT_WithFilename;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.IterSaveGT          := True;
  Inp.IterSaveGTInterval  := 10;
  Inp.IterSaveGTSaves     := 3;
  Inp.IterSaveGTFNEnabled := True;
  Inp.IterSaveGTFNPath    := 'myfile';
  Res := Build(Inp);
  AssertTrue('Must contain -S flag',      Pos('-S', Res) > 0);
  AssertTrue('Must contain -k myfile',    Pos('-k myfile', Res) > 0);
end;

// ---------------------------------------------------------------------------
// Tests — -m (message log)
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.TestMessageLog;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LogMLFNEnabled := True;
  Inp.LogMLFNPath    := 'C:\Logs\msglog.txt';
  Res := Build(Inp);
  AssertTrue('Must contain -m flag', Pos('-m ', Res) > 0);
  AssertTrue('Must contain path',    Pos('msglog.txt', Res) > 0);
end;

// ---------------------------------------------------------------------------
// Tests — -b (battle set)
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.TestBSByNum_Index0;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.BSByNum    := True;
  Inp.BSNumIndex := 0;
  AssertEquals('-b 1', Build(Inp));
end;

procedure TCmdLineBuilderTests.TestBSByNum_Index4;
var
  Inp: THCGECmdLineInputs;
begin
  // Index 4 skips ID 5 → should emit -b 6
  Inp := EmptyInputs;
  Inp.BSByNum    := True;
  Inp.BSNumIndex := 4;
  AssertEquals('-b 6', Build(Inp));
end;

procedure TCmdLineBuilderTests.TestBSByNum_Index30;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.BSByNum    := True;
  Inp.BSNumIndex := 30;
  AssertEquals('-b 32', Build(Inp));
end;

procedure TCmdLineBuilderTests.TestBSByNum_Index31;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.BSByNum    := True;
  Inp.BSNumIndex := 31;
  AssertEquals('-b 35', Build(Inp));
end;

procedure TCmdLineBuilderTests.TestBSByFile;
var
  Inp: THCGECmdLineInputs;
  Res: String;
  F: String;
begin
  F := ExistingFile;
  Inp := EmptyInputs;
  Inp.BSByFile   := True;
  Inp.BSFilePath := F;
  Res := Build(Inp);
  AssertTrue('Must contain -b "..."', Pos('-b "', Res) > 0);
  AssertTrue('Must contain file path', Pos(F, Res) > 0);
end;

procedure TCmdLineBuilderTests.TestBSNumPrecedence;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  // When both BSByNum and BSByFile are true, numeric lookup wins.
  Inp := EmptyInputs;
  Inp.BSByNum    := True;
  Inp.BSNumIndex := 0;
  Inp.BSByFile   := True;
  Inp.BSFilePath := ExistingFile;
  Res := Build(Inp);
  AssertTrue('Numeric must win: -b 1',  Pos('-b 1', Res) > 0);
  // The file-path form uses double quotes; make sure that form does NOT appear.
  AssertEquals('File path form must not appear', 0, Pos('-b "', Res));
end;

// ---------------------------------------------------------------------------
// Tests — -c (scenario)
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.TestScenByNum;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.ScenByNum    := True;
  Inp.ScenNumIndex := 0;
  AssertEquals('-c 1', Build(Inp));
end;

procedure TCmdLineBuilderTests.TestScenByNum_Max;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.ScenByNum    := True;
  Inp.ScenNumIndex := 21;
  AssertEquals('-c 22', Build(Inp));
end;

procedure TCmdLineBuilderTests.TestScenByFile;
var
  Inp: THCGECmdLineInputs;
  Res: String;
  F: String;
begin
  F := ExistingFile;
  Inp := EmptyInputs;
  Inp.ScenByFile   := True;
  Inp.ScenFilePath := F;
  Res := Build(Inp);
  AssertTrue('Must contain -c "..."', Pos('-c "', Res) > 0);
  AssertTrue('Must contain file path', Pos(F, Res) > 0);
end;

// ---------------------------------------------------------------------------
// Tests — -v (save file)
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.TestSavFile;
var
  Inp: THCGECmdLineInputs;
  Res: String;
  F: String;
begin
  F := ExistingFile;
  Inp := EmptyInputs;
  Inp.SavFNEnabled := True;
  Inp.SavFNPath    := F;
  Res := Build(Inp);
  AssertTrue('Must contain -v "..."', Pos('-v "', Res) > 0);
  AssertTrue('Must contain file path', Pos(F, Res) > 0);
end;

// ---------------------------------------------------------------------------
// Tests — -d/-r (CD databases)
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.TestCDBresFN;
var
  Inp: THCGECmdLineInputs;
  Res: String;
  F: String;
begin
  F := ExistingFile;
  Inp := EmptyInputs;
  Inp.CDBresFNEnabled := True;
  Inp.CDBresFNPath    := F;
  Res := Build(Inp);
  AssertTrue('Must contain -d "..."', Pos('-d "', Res) > 0);
  AssertTrue('Must contain file path', Pos(F, Res) > 0);
end;

procedure TCmdLineBuilderTests.TestCDBrsrFN;
var
  Inp: THCGECmdLineInputs;
  Res: String;
  F: String;
begin
  F := ExistingFile;
  Inp := EmptyInputs;
  Inp.CDBrsrFNEnabled := True;
  Inp.CDBrsrFNPath    := F;
  Res := Build(Inp);
  AssertTrue('Must contain -r "..."', Pos('-r "', Res) > 0);
  AssertTrue('Must contain file path', Pos(F, Res) > 0);
end;

// ---------------------------------------------------------------------------
// Tests — -g (game options)
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.TestGameOpts_NewGE;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.GameOptsEnabled    := True;
  Inp.GOUseSideChecklist := False;  // New GE: combobox
  // All indices default to 0 → 9 zeros
  Res := Build(Inp);
  AssertTrue('Must contain -g 000000000', Pos('-g 000000000', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestGameOpts_OldGE;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.GameOptsEnabled    := True;
  Inp.GOUseSideChecklist := True;
  Inp.GOSidesBitmask     := 3;  // First digit must be '3'
  Res := Build(Inp);
  AssertTrue('First digit must be 3', Pos('-g 3', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestGameOpts_MixedIndices;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.GameOptsEnabled    := True;
  Inp.GOUseSideChecklist := False;
  Inp.GOSideIndex        := 1;
  Inp.GONukeRelIndex     := 2;
  Inp.GOSnorkSubIndex    := 0;
  Inp.GORealWeathIndex   := 1;
  Inp.GOMaintFailIndex   := 0;
  Inp.GOFullOrdIndex     := 3;
  Inp.GOFormPatIndex     := 0;
  Inp.GOExitIndex        := 0;
  Inp.GOTimeCompIndex    := 2;
  // Expected: 1,2,0,1,0,3,0,0,2 → '120103002'
  Res := Build(Inp);
  AssertTrue('Must contain -g 120103002', Pos('-g 120103002', Res) > 0);
end;

// ---------------------------------------------------------------------------
// Tests — -p (CPU affinity)
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.TestAffinity_Index0;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.AffinityEnabled := True;
  Inp.AffinityIndex   := 0;
  AssertEquals('-p 1', Build(Inp));
end;

procedure TCmdLineBuilderTests.TestAffinity_Index1;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.AffinityEnabled := True;
  Inp.AffinityIndex   := 1;
  AssertEquals('-p 2', Build(Inp));
end;

procedure TCmdLineBuilderTests.TestAffinity_Index2;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.AffinityEnabled := True;
  Inp.AffinityIndex   := 2;
  AssertEquals('-p 4', Build(Inp));
end;

procedure TCmdLineBuilderTests.TestAffinity_Index3;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.AffinityEnabled := True;
  Inp.AffinityIndex   := 3;
  AssertEquals('-p 8', Build(Inp));
end;

// ---------------------------------------------------------------------------
// Tests — -u (process priority)
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.TestPriority_Index0;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.PriorityEnabled := True;
  Inp.PriorityIndex   := 0;
  AssertEquals('-u -2', Build(Inp));
end;

procedure TCmdLineBuilderTests.TestPriority_Index2;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.PriorityEnabled := True;
  Inp.PriorityIndex   := 2;
  AssertEquals('-u 0', Build(Inp));
end;

procedure TCmdLineBuilderTests.TestPriority_Index4;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.PriorityEnabled := True;
  Inp.PriorityIndex   := 4;
  AssertEquals('-u 2', Build(Inp));
end;

// ---------------------------------------------------------------------------
// Tests — Integration
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.TestFlagOrdering;
var
  Inp: THCGECmdLineInputs;
  Res: String;
  PosF, PosL, PosI_flag, PosH, PosSmall_i, PosA, PosT, PosS_lower,
  PosS_upper, PosM, PosX, PosE, PosB, PosC, PosV, PosD, PosR, PosG,
  PosP, PosU: Integer;
begin
  // Set a representative flag from each group so all groups are present.
  Inp := EmptyInputs;

  // -f
  Inp.LogFN     := True;
  Inp.LogFNPath := TempDir + 'order_test.log'; // directory exists

  // -l
  Inp.LoggingEnabled := True;
  Inp.LogAll         := True;

  // -I
  Inp.DebugEnabled := True;
  Inp.DebugI32501  := True;

  // -h
  Inp.WindowHideEnabled := True;
  Inp.WHGroup           := True;

  // -i
  Inp.IgnoreReg := True;

  // -a
  Inp.AutoPlay := True;

  // -t
  Inp.SavIntEnabled := True;
  Inp.SavIntText    := '2.0';

  // -s
  Inp.IterSave := True;

  // -S
  Inp.IterSaveGT         := True;
  Inp.IterSaveGTInterval := 50;
  Inp.IterSaveGTSaves    := 10;

  // -m
  Inp.LogMLFNEnabled := True;
  Inp.LogMLFNPath    := 'msglog.txt';

  // -x
  Inp.SubAIDisable := True;

  // -e
  Inp.Eff115Old := True;

  // -b
  Inp.BSByNum    := True;
  Inp.BSNumIndex := 0;

  // -c
  Inp.ScenByNum    := True;
  Inp.ScenNumIndex := 0;

  // -v
  Inp.SavFNEnabled := True;
  Inp.SavFNPath    := ExistingFile;

  // -d
  Inp.CDBresFNEnabled := True;
  Inp.CDBresFNPath    := ExistingFile;

  // -r
  Inp.CDBrsrFNEnabled := True;
  Inp.CDBrsrFNPath    := ExistingFile;

  // -g
  Inp.GameOptsEnabled := True;

  // -p
  Inp.AffinityEnabled := True;
  Inp.AffinityIndex   := 0;

  // -u
  Inp.PriorityEnabled := True;
  Inp.PriorityIndex   := 2;

  Res := Build(Inp);

  PosF        := Pos('-f ',    Res);
  PosL        := Pos('-l ',    Res);
  PosI_flag   := Pos('-I ',    Res);
  PosH        := Pos('-h ',    Res);
  PosSmall_i  := Pos('-i ',    Res);
  PosA        := Pos('-a ',    Res);
  PosT        := Pos('-t ',    Res);
  PosS_lower  := Pos('-s ',    Res);
  PosS_upper  := Pos('-S',     Res);
  PosM        := Pos('-m ',    Res);
  PosX        := Pos('-x ',    Res);
  PosE        := Pos('-e ',    Res);
  PosB        := Pos('-b ',    Res);
  PosC        := Pos('-c ',    Res);
  PosV        := Pos('-v "',   Res);
  PosD        := Pos('-d "',   Res);
  PosR        := Pos('-r "',   Res);
  PosG        := Pos('-g ',    Res);
  PosP        := Pos('-p ',    Res);
  PosU        := Pos('-u ',    Res);

  AssertTrue('-f must appear', PosF > 0);
  AssertTrue('-l must appear', PosL > 0);
  AssertTrue('-I must appear', PosI_flag > 0);
  AssertTrue('-h must appear', PosH > 0);
  AssertTrue('-i must appear', PosSmall_i > 0);
  AssertTrue('-a must appear', PosA > 0);
  AssertTrue('-t must appear', PosT > 0);
  AssertTrue('-s must appear', PosS_lower > 0);
  AssertTrue('-S must appear', PosS_upper > 0);
  AssertTrue('-m must appear', PosM > 0);
  AssertTrue('-x must appear', PosX > 0);
  AssertTrue('-e must appear', PosE > 0);
  AssertTrue('-b must appear', PosB > 0);
  AssertTrue('-c must appear', PosC > 0);
  AssertTrue('-v must appear', PosV > 0);
  AssertTrue('-d must appear', PosD > 0);
  AssertTrue('-r must appear', PosR > 0);
  AssertTrue('-g must appear', PosG > 0);
  AssertTrue('-p must appear', PosP > 0);
  AssertTrue('-u must appear', PosU > 0);

  // Verify ordering: each flag must appear before the next
  AssertTrue('-f before -l',   PosF       < PosL);
  AssertTrue('-l before -I',   PosL       < PosI_flag);
  AssertTrue('-I before -h',   PosI_flag  < PosH);
  AssertTrue('-h before -i',   PosH       < PosSmall_i);
  AssertTrue('-i before -a',   PosSmall_i < PosA);
  AssertTrue('-a before -t',   PosA       < PosT);
  AssertTrue('-t before -s',   PosT       < PosS_lower);
  AssertTrue('-s before -S',   PosS_lower < PosS_upper);
  AssertTrue('-S before -m',   PosS_upper < PosM);
  AssertTrue('-m before -x',   PosM       < PosX);
  AssertTrue('-x before -e',   PosX       < PosE);
  AssertTrue('-e before -b',   PosE       < PosB);
  AssertTrue('-b before -c',   PosB       < PosC);
  AssertTrue('-c before -v',   PosC       < PosV);
  AssertTrue('-v before -d',   PosV       < PosD);
  AssertTrue('-d before -r',   PosD       < PosR);
  AssertTrue('-r before -g',   PosR       < PosG);
  AssertTrue('-g before -p',   PosG       < PosP);
  AssertTrue('-p before -u',   PosP       < PosU);
end;

procedure TCmdLineBuilderTests.TestFullRealistic;
var
  Inp: THCGECmdLineInputs;
  Res: String;
  F: String;
begin
  F := ExistingFile;

  Inp := EmptyInputs;

  // Log file
  Inp.LogFN     := True;
  Inp.LogFNPath := TempDir + 'hcltest.log';

  // Logging — specific flags
  Inp.LoggingEnabled := True;
  Inp.LogRadar       := True;
  Inp.LogMission     := True;

  // Debug
  Inp.DebugEnabled := True;
  Inp.DebugI32501  := True;

  // Window hide
  Inp.WindowHideEnabled := True;
  Inp.WHMiniReport      := True;
  Inp.WHMessageLog      := True;

  // Simple flags
  Inp.AutoPlay     := True;
  Inp.SubAIDisable := True;

  // Save interval
  Inp.SavIntEnabled := True;
  Inp.SavIntText    := '0.5';

  // Iterative save GT
  Inp.IterSaveGT          := True;
  Inp.IterSaveGTInterval  := 200;
  Inp.IterSaveGTSaves     := 50;
  Inp.IterSaveGTFNEnabled := True;
  Inp.IterSaveGTFNPath    := 'saves\mygame';

  // Battle set by number
  Inp.BSByNum    := True;
  Inp.BSNumIndex := 4;  // → ID 6

  // Save file
  Inp.SavFNEnabled := True;
  Inp.SavFNPath    := F;

  // Game options
  Inp.GameOptsEnabled    := True;
  Inp.GOUseSideChecklist := False;
  Inp.GOSideIndex        := 1;

  // CPU affinity
  Inp.AffinityEnabled := True;
  Inp.AffinityIndex   := 1;  // → -p 2

  // Process priority
  Inp.PriorityEnabled := True;
  Inp.PriorityIndex   := 3;  // → -u 1

  Res := Build(Inp);

  // Spot-check key fragments
  AssertTrue('Contains -f',           Pos('-f "',         Res) > 0);
  AssertTrue('Contains -l radar',     Pos('-l radar',     Res) > 0);
  AssertTrue('Contains -l mission',   Pos('-l mission',   Res) > 0);
  AssertTrue('Contains -I I32501',    Pos('-I I32501',    Res) > 0);
  AssertTrue('Contains -h minireport',Pos('-h minireport',Res) > 0);
  AssertTrue('Contains -h messagelog',Pos('-h messagelog',Res) > 0);
  AssertTrue('Contains -a',           Pos('-a ',          Res) > 0);
  AssertTrue('Contains -t 0.5',       Pos('-t 0.5',       Res) > 0);
  AssertTrue('Contains -S002000050',  Pos('-S002000050',  Res) > 0);
  AssertTrue('Contains -k saves',     Pos('-k saves',     Res) > 0);
  AssertTrue('Contains -x',           Pos('-x ',          Res) > 0);
  AssertTrue('Contains -b 6',         Pos('-b 6 ',        Res) > 0);
  AssertTrue('Contains -v "',         Pos('-v "',         Res) > 0);
  AssertTrue('Contains -g 1',         Pos('-g 1',         Res) > 0);
  AssertTrue('Contains -p 2',         Pos('-p 2',         Res) > 0);
  AssertTrue('Contains -u 1',         Pos('-u 1',         Res) > 0);
end;

// ---------------------------------------------------------------------------
// Tests — Gap coverage
// ---------------------------------------------------------------------------

// -f with empty path: LogFN=True but LogFNPath='' → no -f
procedure TCmdLineBuilderTests.TestLogFile_EmptyPath;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.LogFN     := True;
  Inp.LogFNPath := '';
  AssertEquals('-f must be suppressed when LogFNPath is empty', '', Build(Inp));
end;

// -l master switch off: individual flags set but LoggingEnabled=False → nothing
procedure TCmdLineBuilderTests.TestLog_MasterSwitchOff;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := False;
  Inp.LogAll         := True;
  Inp.LogRadar       := True;
  Inp.LogMission     := True;
  AssertEquals('LoggingEnabled=False must suppress all -l flags', '', Build(Inp));
end;

// Individual -l flags
procedure TCmdLineBuilderTests.TestLogIndividual_AcAI;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogAcAI        := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l ac_ai', Pos('-l ac_ai', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogIndividual_SubAI;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogSubAI       := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l sub_ai', Pos('-l sub_ai', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogIndividual_EmconAI;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogEmconAI     := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l emcon_ai', Pos('-l emcon_ai', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogIndividual_Sonar;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogSonar       := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l sonar', Pos('-l sonar', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogIndividual_Refuel;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogRefuel      := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l refuel', Pos('-l refuel', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogIndividual_Mission;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogMission     := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l mission', Pos('-l mission', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogIndividual_Intercept;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogIntercept   := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l intercept', Pos('-l intercept', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogIndividual_ResFile;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogResFile     := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l res_file', Pos('-l res_file', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogIndividual_Load;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogLoad        := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l load', Pos('-l load', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogIndividual_Radar;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogRadar       := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l radar', Pos('-l radar', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogIndividual_ECM;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogECM         := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l ecm', Pos('-l ecm', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogIndividual_Missile;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogMissile     := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l missile', Pos('-l missile', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogIndividual_AreaDef;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogAreaDef     := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l areadef', Pos('-l areadef', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogIndividual_Perf;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogPerf        := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l perf', Pos('-l perf', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogIndividual_BadDB;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogBadDB       := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l bad_db', Pos('-l bad_db', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogIndividual_Graphic;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogGraphic     := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l graphic', Pos('-l graphic', Res) > 0);
end;

procedure TCmdLineBuilderTests.TestLogIndividual_Unit;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogUnit        := True;
  Res := Build(Inp);
  AssertTrue('Must contain -l unit', Pos('-l unit', Res) > 0);
end;

// -h all 5 sub-flags at once
procedure TCmdLineBuilderTests.TestWindowHideAllFive;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.WindowHideEnabled := True;
  Inp.WHGroup           := True;
  Inp.WHUnit            := True;
  Inp.WHStratMap        := True;
  Inp.WHMiniReport      := True;
  Inp.WHMessageLog      := True;
  Res := Build(Inp);
  AssertTrue('Must contain -h group',        Pos('-h group',        Res) > 0);
  AssertTrue('Must contain -h unit',         Pos('-h unit',         Res) > 0);
  AssertTrue('Must contain -h strategicmap', Pos('-h strategicmap', Res) > 0);
  AssertTrue('Must contain -h minireport',   Pos('-h minireport',   Res) > 0);
  AssertTrue('Must contain -h messagelog',   Pos('-h messagelog',   Res) > 0);
end;

// -S with zero interval and saves: should emit -S000000000
procedure TCmdLineBuilderTests.TestIterSaveGT_ZeroValues;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.IterSaveGT         := True;
  Inp.IterSaveGTInterval := 0;
  Inp.IterSaveGTSaves    := 0;
  Res := Build(Inp);
  AssertTrue('Must contain -S000000000', Pos('-S000000000', Res) > 0);
end;

// -S without -k: IterSaveGTFNEnabled=False → no -k emitted
procedure TCmdLineBuilderTests.TestIterSaveGT_NoFilename;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.IterSaveGT          := True;
  Inp.IterSaveGTInterval  := 10;
  Inp.IterSaveGTSaves     := 3;
  Inp.IterSaveGTFNEnabled := False;
  Inp.IterSaveGTFNPath    := 'somefile';
  Res := Build(Inp);
  AssertTrue('Must contain -S flag',    Pos('-S', Res) > 0);
  AssertEquals('-k must not appear', 0, Pos('-k ', Res));
end;

// -b with BSNumIndex out of range (too high)
procedure TCmdLineBuilderTests.TestBSByNum_OutOfRange_High;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.BSByNum    := True;
  Inp.BSNumIndex := 32;   // High(BS_INDEX_TO_ID) = 31
  AssertEquals('Out-of-range BSNumIndex must suppress -b', '', Build(Inp));
end;

// -b with BSNumIndex=-1 (negative)
procedure TCmdLineBuilderTests.TestBSByNum_OutOfRange_Negative;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.BSByNum    := True;
  Inp.BSNumIndex := -1;
  AssertEquals('Negative BSNumIndex must suppress -b', '', Build(Inp));
end;

// -c with ScenNumIndex=-1: ScenByNum=True but no selection → no -c
procedure TCmdLineBuilderTests.TestScenByNum_NegativeIndex;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.ScenByNum    := True;
  Inp.ScenNumIndex := -1;
  AssertEquals('ScenNumIndex=-1 must suppress -c', '', Build(Inp));
end;

// -v with non-existent file → no -v
procedure TCmdLineBuilderTests.TestSavFile_NonExistent;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.SavFNEnabled := True;
  Inp.SavFNPath    := NonExistentPath;
  AssertEquals('Non-existent save file must suppress -v', '', Build(Inp));
end;

// -d with non-existent file → no -d
procedure TCmdLineBuilderTests.TestCDBresFN_NonExistent;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.CDBresFNEnabled := True;
  Inp.CDBresFNPath    := NonExistentPath;
  AssertEquals('Non-existent CDBresFN must suppress -d', '', Build(Inp));
end;

// -r with non-existent file → no -r
procedure TCmdLineBuilderTests.TestCDBrsrFN_NonExistent;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.CDBrsrFNEnabled := True;
  Inp.CDBrsrFNPath    := NonExistentPath;
  AssertEquals('Non-existent CDBrsrFN must suppress -r', '', Build(Inp));
end;

// -p with AffinityIndex out of range (too high)
procedure TCmdLineBuilderTests.TestAffinity_OutOfRange_High;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.AffinityEnabled := True;
  Inp.AffinityIndex   := 4;   // High(AFFINITY_MASK) = 3
  AssertEquals('AffinityIndex=4 must suppress -p', '', Build(Inp));
end;

// -p with AffinityIndex=-1 (negative)
procedure TCmdLineBuilderTests.TestAffinity_OutOfRange_Negative;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.AffinityEnabled := True;
  Inp.AffinityIndex   := -1;
  AssertEquals('AffinityIndex=-1 must suppress -p', '', Build(Inp));
end;

// -u with PriorityIndex=-1 → no -u
procedure TCmdLineBuilderTests.TestPriority_NegativeIndex;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.PriorityEnabled := True;
  Inp.PriorityIndex   := -1;
  AssertEquals('PriorityIndex=-1 must suppress -u', '', Build(Inp));
end;

// -g with all 9 digits non-zero and distinct
procedure TCmdLineBuilderTests.TestGameOpts_AllNineDigits;
var
  Inp: THCGECmdLineInputs;
  Res: String;
begin
  Inp := EmptyInputs;
  Inp.GameOptsEnabled    := True;
  Inp.GOUseSideChecklist := False;
  Inp.GOSideIndex        := 1;
  Inp.GONukeRelIndex     := 2;
  Inp.GOSnorkSubIndex    := 3;
  Inp.GORealWeathIndex   := 1;
  Inp.GOMaintFailIndex   := 2;
  Inp.GOFullOrdIndex     := 3;
  Inp.GOFormPatIndex     := 1;
  Inp.GOExitIndex        := 2;
  Inp.GOTimeCompIndex    := 3;
  // Expected: 1,2,3,1,2,3,1,2,3 → '123123123'
  Res := Build(Inp);
  AssertTrue('Must contain -g 123123123', Pos('-g 123123123', Res) > 0);
end;

// ---------------------------------------------------------------------------
// Tests — Scenario Editor (BuildCommandLineSE)
// ---------------------------------------------------------------------------

procedure TCmdLineBuilderTests.TestSE_Empty;
begin
  AssertEquals('nothing selected', '', FBuilder.BuildCommandLineSE(EmptyInputs));
end;

procedure TCmdLineBuilderTests.TestSE_GEOnlyOptionsLeftOut;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.LogFN := True;                Inp.LogFNPath := TempDir + 'hc.log';
  Inp.DebugEnabled := True;         Inp.DebugI32501 := True;
  Inp.WindowHideEnabled := True;    Inp.WHGroup := True;
  Inp.IgnoreReg := True;
  Inp.AutoPlay := True;
  Inp.SubAIDisable := True;
  Inp.Eff115Old := True;
  Inp.SavIntEnabled := True;        Inp.SavIntText := '30';
  Inp.IterSave := True;
  Inp.BSByFile := True;             Inp.BSFilePath := ExistingFile;
  Inp.ScenByNum := True;            Inp.ScenNumIndex := 2;
  Inp.SavFNEnabled := True;         Inp.SavFNPath := ExistingFile;
  Inp.GameOptsEnabled := True;
  AssertTrue('the GE gets plenty', Build(Inp) <> '');
  // In particular the SE's -t is a working directory: "-t 30" would chdir to "30".
  AssertEquals('the SE gets none of it', '', FBuilder.BuildCommandLineSE(Inp));
end;

procedure TCmdLineBuilderTests.TestSE_LoggingWithoutPerf;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogRadar := True;
  Inp.LogPerf  := True;
  Inp.LogUnit  := True;
  AssertEquals('perf left out', '-l radar -l unit', FBuilder.BuildCommandLineSE(Inp));
  AssertTrue('the GE line still has perf', Pos('-l perf', Build(Inp)) > 0);

  Inp.LoggingEnabled := False;
  AssertEquals('master switch off', '', FBuilder.BuildCommandLineSE(Inp));
end;

procedure TCmdLineBuilderTests.TestSE_LogAll;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled := True;
  Inp.LogAll   := True;
  Inp.LogRadar := True;
  AssertEquals('all, alone', '-l all', FBuilder.BuildCommandLineSE(Inp));
end;

procedure TCmdLineBuilderTests.TestSE_BattleSetAndDatabases;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.LoggingEnabled  := True;
  Inp.LogLoad         := True;
  Inp.BSByNum         := True;
  Inp.BSNumIndex      := 0;
  Inp.CDBresFNEnabled := True;
  Inp.CDBresFNPath    := ExistingFile;
  Inp.CDBrsrFNEnabled := True;
  Inp.CDBrsrFNPath    := ExistingFile;
  AssertEquals('-l, -b, -d, -r',
    '-l load -b ' + IntToStr(BS_INDEX_TO_ID[0]) +
    ' -d "' + ExistingFile + '" -r "' + ExistingFile + '"',
    FBuilder.BuildCommandLineSE(Inp));

  Inp.BSNumIndex := High(BS_INDEX_TO_ID) + 1;
  AssertEquals('an out-of-range battleset is dropped',
    '-l load -d "' + ExistingFile + '" -r "' + ExistingFile + '"',
    FBuilder.BuildCommandLineSE(Inp));
end;

procedure TCmdLineBuilderTests.TestSE_MissingDatabaseFilesLeftOut;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  Inp.CDBresFNEnabled := True;
  Inp.CDBresFNPath    := TempDir + 'no_such_commondb.res';
  Inp.CDBrsrFNEnabled := False;
  Inp.CDBrsrFNPath    := ExistingFile;  // switched off
  AssertEquals('neither passed', '', FBuilder.BuildCommandLineSE(Inp));
end;

procedure TCmdLineBuilderTests.TestBSNumberExplicit;
var
  Inp: THCGECmdLineInputs;
begin
  Inp := EmptyInputs;
  AssertEquals('default: none', 0, Inp.BSNumber);
  Inp.BSNumber := 22;
  Inp.BSByNum := True;
  Inp.BSNumIndex := 0;
  Inp.BSByFile := True;
  Inp.BSFilePath := ExistingFile;
  Inp.ScenByFile := True;
  Inp.ScenFilePath := ExistingFile;
  AssertTrue('GE: -b 22 then -c', Pos('-b 22 -c "', Build(Inp)) > 0);
  AssertEquals('GE: only one -b', 0, Pos('-b ' + IntToStr(BS_INDEX_TO_ID[0]) + ' ', Build(Inp)));
  AssertEquals('SE: -b 22', '-b 22', FBuilder.BuildCommandLineSE(Inp));
  Inp.BSNumber := 120;
  AssertTrue('a number the picker lacks', Pos('-b 120 ', Build(Inp)) > 0);
end;

// ---------------------------------------------------------------------------
// Registration
// ---------------------------------------------------------------------------

initialization
  RegisterTest(TCmdLineBuilderTests);

end.
