unit test_FileOrganization;

{$mode objfpc}{$H+}

/// <summary>
/// test_FileOrganization — FPCUnit tests for uHCLFileOrganization.
///
/// Exercises path-construction (GetManagedRoot, GetCategoryPath,
/// GetCachePath, BattleSet/PlatformDB nested paths), directory-creation
/// helpers (EnsureCategoryExists, EnsureAllCategoriesExist,
/// EnsureScenarioPath) and directory/file listing.
///
/// Each test gets a private temp directory created in SetUp and
/// recursively deleted in TearDown so no test state leaks.
/// </summary>

interface

uses
  Classes, SysUtils, FileUtil,
  fpcunit, testregistry,
  uHCLFileOrganization;

type
  /// <summary>
  /// FPCUnit test suite for <see cref="IHCLFileOrganization"/> and its
  /// factory function <see cref="CreateFileOrganization"/>.
  /// </summary>
  TTestFileOrganization = class(TTestCase)
  private
    FTempDir: String;
    FOrg: IHCLFileOrganization;

    /// <summary>
    /// Returns the expected managed-root path (<c>FTempDir\HCLauncher</c>).
    /// </summary>
    function ManagedRoot: String;

    /// <summary>
    /// Returns the expected category path for <paramref name="ACatName"/>.
    /// </summary>
    function CatPath(const ACatName: String): String;

    /// <summary>
    /// Creates an empty file at <paramref name="APath"/> so that listing
    /// tests have something to discover.
    /// </summary>
    procedure TouchFile(const APath: String);

  protected
    /// <summary>Creates a unique temp directory and the IHCLFileOrganization instance under test.</summary>
    procedure SetUp; override;

    /// <summary>Recursively deletes the temp directory created in SetUp.</summary>
    procedure TearDown; override;

  published
    // --- Root and category paths ----------------------------------------

    /// <summary>
    /// GetManagedRoot must return &lt;basedir&gt;\HCLauncher.
    /// </summary>
    procedure TestManagedRoot;

    /// <summary>
    /// GetCategoryPath(hclBattleSets) must return &lt;root&gt;\BattleSets.
    /// </summary>
    procedure TestCategoryPath_BattleSets;

    /// <summary>
    /// GetCategoryPath returns the correct subdirectory name for every
    /// THCLCategory value by cross-checking against HCL_CATEGORY_NAMES.
    /// </summary>
    procedure TestCategoryPath_AllCategories;

    /// <summary>
    /// GetCachePath(hclSounds) must return &lt;root&gt;\Sounds\cache.
    /// </summary>
    procedure TestCachePath;

    /// <summary>
    /// GetCachePath appends \cache to every category path.
    /// </summary>
    procedure TestCachePath_AllCategories;

    // --- BattleSet nested paths ------------------------------------------

    /// <summary>
    /// GetBattleSetPath('GIUK') must return &lt;root&gt;\BattleSets\GIUK.
    /// </summary>
    procedure TestBattleSetPath;

    /// <summary>
    /// GetBattleSetScenariosPath('GIUK') appends \Scenarios.
    /// </summary>
    procedure TestBattleSetScenariosPath;

    /// <summary>
    /// GetBattleSetScenarioPath('GIUK','Day1') appends \Scenarios\Day1.
    /// </summary>
    procedure TestBattleSetScenarioPath;

    /// <summary>
    /// GetBattleSetSavedGamesPath('GIUK','Day1') appends \Scenarios\Day1\SavedGames.
    /// </summary>
    procedure TestBattleSetSavedGamesPath;

    /// <summary>
    /// GetBattleSetAutoSavesPath('GIUK','Day1') appends \Scenarios\Day1\SavedGames\AutoSaves.
    /// </summary>
    procedure TestBattleSetAutoSavesPath;

    // --- PlatformDB nested paths -----------------------------------------

    /// <summary>
    /// GetPlatformDBPath('HCDB-160414') must return &lt;root&gt;\PlatformDBs\HCDB-160414.
    /// </summary>
    procedure TestPlatformDBPath;

    /// <summary>
    /// GetPlatformDBScenariosPath('HCDB-160414') appends \Scenarios.
    /// </summary>
    procedure TestPlatformDBScenariosPath;

    /// <summary>
    /// GetPlatformDBScenarioPath('HCDB-160414','Sc1') appends \Scenarios\Sc1.
    /// </summary>
    procedure TestPlatformDBScenarioPath;

    /// <summary>
    /// GetPlatformDBSavedGamesPath('HCDB-160414','Sc1') appends \Scenarios\Sc1\SavedGames.
    /// </summary>
    procedure TestPlatformDBSavedGamesPath;

    /// <summary>
    /// GetPlatformDBAutoSavesPath('HCDB-160414','Sc1') appends \Scenarios\Sc1\SavedGames\AutoSaves.
    /// </summary>
    procedure TestPlatformDBAutoSavesPath;

    // --- Directory creation ----------------------------------------------

    /// <summary>
    /// EnsureCategoryExists(hclBattleSets) creates both the category dir
    /// and its cache subdirectory, and returns True.
    /// </summary>
    procedure TestEnsureCategoryExists;

    /// <summary>
    /// EnsureAllCategoriesExist creates all 7 category directories and all
    /// 7 cache subdirectories.
    /// </summary>
    procedure TestEnsureAllCategoriesExist;

    /// <summary>
    /// EnsureScenarioPath with IsCommonDB=False creates the full
    /// BattleSets\X\Scenarios\Y\SavedGames\AutoSaves tree.
    /// </summary>
    procedure TestEnsureScenarioPath_BattleSet;

    /// <summary>
    /// EnsureScenarioPath with IsCommonDB=True creates the full
    /// PlatformDBs\V\Scenarios\Y\SavedGames\AutoSaves tree.
    /// </summary>
    procedure TestEnsureScenarioPath_PlatformDB;

    // --- Listing ---------------------------------------------------------

    /// <summary>
    /// ListSubdirectories on a nonexistent path returns an empty array.
    /// </summary>
    procedure TestListSubdirectories_Empty;

    /// <summary>
    /// ListSubdirectories returns exactly the immediate subdirectory names
    /// that were physically created under the queried path.
    /// </summary>
    procedure TestListSubdirectories;

    /// <summary>
    /// ListFiles on a nonexistent path returns an empty array.
    /// </summary>
    procedure TestListFiles_Empty;

    /// <summary>
    /// ListFiles with a fixed extension filter returns only files whose
    /// extension matches.
    /// </summary>
    procedure TestListFiles_WithExtFilter;

    /// <summary>
    /// ListFiles with a wildcard filter (.sc?) matches single-char
    /// variants (.scs, .scq) but rejects longer extensions (.scxx).
    /// </summary>
    procedure TestListFiles_WildcardFilter;

    // --- Edge cases ------------------------------------------------------

    /// <summary>
    /// A base directory supplied with a trailing path delimiter must still
    /// produce paths that contain no double delimiters.
    /// </summary>
    procedure TestBaseDirTrailingPathDelim;

    /// <summary>
    /// BattleSet and scenario names containing spaces are handled correctly
    /// in all path-construction methods.
    /// </summary>
    procedure TestNamesWithSpaces;
  end;

implementation

{ --------------------------------------------------------------------------- }
{ Helpers                                                                      }
{ --------------------------------------------------------------------------- }

function TTestFileOrganization.ManagedRoot: String;
begin
  Result := FTempDir + PathDelim + HCL_MANAGED_DIR;
end;

function TTestFileOrganization.CatPath(const ACatName: String): String;
begin
  Result := ManagedRoot + PathDelim + ACatName;
end;

procedure TTestFileOrganization.TouchFile(const APath: String);
var
  FS: TFileStream;
begin
  FS := TFileStream.Create(APath, fmCreate);
  FS.Free;
end;

{ --------------------------------------------------------------------------- }
{ SetUp / TearDown                                                             }
{ --------------------------------------------------------------------------- }

procedure TTestFileOrganization.SetUp;
begin
  FTempDir := GetTempDir + 'HCLTest_' + IntToStr(GetTickCount64);
  ForceDirectories(FTempDir);
  FOrg := CreateFileOrganization(FTempDir);
end;

procedure TTestFileOrganization.TearDown;
begin
  FOrg := nil;
  if DirectoryExists(FTempDir) then
    DeleteDirectory(FTempDir, False);
end;

{ --------------------------------------------------------------------------- }
{ Root and category paths                                                      }
{ --------------------------------------------------------------------------- }

procedure TTestFileOrganization.TestManagedRoot;
begin
  AssertEquals('GetManagedRoot',
    FTempDir + PathDelim + HCL_MANAGED_DIR,
    FOrg.GetManagedRoot);
end;

procedure TTestFileOrganization.TestCategoryPath_BattleSets;
begin
  AssertEquals('BattleSets category path',
    CatPath(HCL_CATEGORY_NAMES[hclBattleSets]),
    FOrg.GetCategoryPath(hclBattleSets));
end;

procedure TTestFileOrganization.TestCategoryPath_AllCategories;
var
  Cat: THCLCategory;
begin
  for Cat := Low(THCLCategory) to High(THCLCategory) do
    AssertEquals('Category path for ' + HCL_CATEGORY_NAMES[Cat],
      ManagedRoot + PathDelim + HCL_CATEGORY_NAMES[Cat],
      FOrg.GetCategoryPath(Cat));
end;

procedure TTestFileOrganization.TestCachePath;
begin
  AssertEquals('Cache path for Sounds',
    CatPath(HCL_CATEGORY_NAMES[hclSounds]) + PathDelim + HCL_CACHE_DIR,
    FOrg.GetCachePath(hclSounds));
end;

procedure TTestFileOrganization.TestCachePath_AllCategories;
var
  Cat: THCLCategory;
begin
  for Cat := Low(THCLCategory) to High(THCLCategory) do
    AssertEquals('Cache path for ' + HCL_CATEGORY_NAMES[Cat],
      ManagedRoot + PathDelim + HCL_CATEGORY_NAMES[Cat] + PathDelim + HCL_CACHE_DIR,
      FOrg.GetCachePath(Cat));
end;

{ --------------------------------------------------------------------------- }
{ BattleSet nested paths                                                       }
{ --------------------------------------------------------------------------- }

procedure TTestFileOrganization.TestBattleSetPath;
begin
  AssertEquals('BattleSet path',
    CatPath(HCL_CATEGORY_NAMES[hclBattleSets]) + PathDelim + 'GIUK',
    FOrg.GetBattleSetPath('GIUK'));
end;

procedure TTestFileOrganization.TestBattleSetScenariosPath;
begin
  AssertEquals('BattleSet scenarios path',
    CatPath(HCL_CATEGORY_NAMES[hclBattleSets]) + PathDelim + 'GIUK'
      + PathDelim + HCL_SCENARIOS_DIR,
    FOrg.GetBattleSetScenariosPath('GIUK'));
end;

procedure TTestFileOrganization.TestBattleSetScenarioPath;
begin
  AssertEquals('BattleSet scenario path',
    CatPath(HCL_CATEGORY_NAMES[hclBattleSets]) + PathDelim + 'GIUK'
      + PathDelim + HCL_SCENARIOS_DIR + PathDelim + 'Day1',
    FOrg.GetBattleSetScenarioPath('GIUK', 'Day1'));
end;

procedure TTestFileOrganization.TestBattleSetSavedGamesPath;
begin
  AssertEquals('BattleSet saved games path',
    CatPath(HCL_CATEGORY_NAMES[hclBattleSets]) + PathDelim + 'GIUK'
      + PathDelim + HCL_SCENARIOS_DIR + PathDelim + 'Day1'
      + PathDelim + HCL_SAVEDGAMES_DIR,
    FOrg.GetBattleSetSavedGamesPath('GIUK', 'Day1'));
end;

procedure TTestFileOrganization.TestBattleSetAutoSavesPath;
begin
  AssertEquals('BattleSet auto-saves path',
    CatPath(HCL_CATEGORY_NAMES[hclBattleSets]) + PathDelim + 'GIUK'
      + PathDelim + HCL_SCENARIOS_DIR + PathDelim + 'Day1'
      + PathDelim + HCL_SAVEDGAMES_DIR + PathDelim + HCL_AUTOSAVES_DIR,
    FOrg.GetBattleSetAutoSavesPath('GIUK', 'Day1'));
end;

{ --------------------------------------------------------------------------- }
{ PlatformDB nested paths                                                      }
{ --------------------------------------------------------------------------- }

procedure TTestFileOrganization.TestPlatformDBPath;
begin
  AssertEquals('PlatformDB path',
    CatPath(HCL_CATEGORY_NAMES[hclPlatformDBs]) + PathDelim + 'HCDB-160414',
    FOrg.GetPlatformDBPath('HCDB-160414'));
end;

procedure TTestFileOrganization.TestPlatformDBScenariosPath;
begin
  AssertEquals('PlatformDB scenarios path',
    CatPath(HCL_CATEGORY_NAMES[hclPlatformDBs]) + PathDelim + 'HCDB-160414'
      + PathDelim + HCL_SCENARIOS_DIR,
    FOrg.GetPlatformDBScenariosPath('HCDB-160414'));
end;

procedure TTestFileOrganization.TestPlatformDBScenarioPath;
begin
  AssertEquals('PlatformDB scenario path',
    CatPath(HCL_CATEGORY_NAMES[hclPlatformDBs]) + PathDelim + 'HCDB-160414'
      + PathDelim + HCL_SCENARIOS_DIR + PathDelim + 'Sc1',
    FOrg.GetPlatformDBScenarioPath('HCDB-160414', 'Sc1'));
end;

procedure TTestFileOrganization.TestPlatformDBSavedGamesPath;
begin
  AssertEquals('PlatformDB saved games path',
    CatPath(HCL_CATEGORY_NAMES[hclPlatformDBs]) + PathDelim + 'HCDB-160414'
      + PathDelim + HCL_SCENARIOS_DIR + PathDelim + 'Sc1'
      + PathDelim + HCL_SAVEDGAMES_DIR,
    FOrg.GetPlatformDBSavedGamesPath('HCDB-160414', 'Sc1'));
end;

procedure TTestFileOrganization.TestPlatformDBAutoSavesPath;
begin
  AssertEquals('PlatformDB auto-saves path',
    CatPath(HCL_CATEGORY_NAMES[hclPlatformDBs]) + PathDelim + 'HCDB-160414'
      + PathDelim + HCL_SCENARIOS_DIR + PathDelim + 'Sc1'
      + PathDelim + HCL_SAVEDGAMES_DIR + PathDelim + HCL_AUTOSAVES_DIR,
    FOrg.GetPlatformDBAutoSavesPath('HCDB-160414', 'Sc1'));
end;

{ --------------------------------------------------------------------------- }
{ Directory creation                                                           }
{ --------------------------------------------------------------------------- }

procedure TTestFileOrganization.TestEnsureCategoryExists;
var
  CatDir, CacheDir: String;
begin
  AssertTrue('EnsureCategoryExists returns True',
    FOrg.EnsureCategoryExists(hclBattleSets));

  CatDir   := FOrg.GetCategoryPath(hclBattleSets);
  CacheDir := FOrg.GetCachePath(hclBattleSets);

  AssertTrue('Category dir created: ' + CatDir,   DirectoryExists(CatDir));
  AssertTrue('Cache dir created: '    + CacheDir, DirectoryExists(CacheDir));
end;

procedure TTestFileOrganization.TestEnsureAllCategoriesExist;
var
  Cat: THCLCategory;
begin
  FOrg.EnsureAllCategoriesExist;

  for Cat := Low(THCLCategory) to High(THCLCategory) do
  begin
    AssertTrue('Category dir ' + HCL_CATEGORY_NAMES[Cat],
      DirectoryExists(FOrg.GetCategoryPath(Cat)));
    AssertTrue('Cache dir ' + HCL_CATEGORY_NAMES[Cat],
      DirectoryExists(FOrg.GetCachePath(Cat)));
  end;
end;

procedure TTestFileOrganization.TestEnsureScenarioPath_BattleSet;
begin
  FOrg.EnsureScenarioPath('NORDIC', 'AttackNorth', False);

  AssertTrue('BattleSet root exists',
    DirectoryExists(FOrg.GetBattleSetPath('NORDIC')));
  AssertTrue('Scenarios dir exists',
    DirectoryExists(FOrg.GetBattleSetScenariosPath('NORDIC')));
  AssertTrue('Scenario dir exists',
    DirectoryExists(FOrg.GetBattleSetScenarioPath('NORDIC', 'AttackNorth')));
  AssertTrue('SavedGames dir exists',
    DirectoryExists(FOrg.GetBattleSetSavedGamesPath('NORDIC', 'AttackNorth')));
  AssertTrue('AutoSaves dir exists',
    DirectoryExists(FOrg.GetBattleSetAutoSavesPath('NORDIC', 'AttackNorth')));
end;

procedure TTestFileOrganization.TestEnsureScenarioPath_PlatformDB;
begin
  FOrg.EnsureScenarioPath('HCDB-160414', 'Sc1', True);

  AssertTrue('PlatformDB root exists',
    DirectoryExists(FOrg.GetPlatformDBPath('HCDB-160414')));
  AssertTrue('Scenarios dir exists',
    DirectoryExists(FOrg.GetPlatformDBScenariosPath('HCDB-160414')));
  AssertTrue('Scenario dir exists',
    DirectoryExists(FOrg.GetPlatformDBScenarioPath('HCDB-160414', 'Sc1')));
  AssertTrue('SavedGames dir exists',
    DirectoryExists(FOrg.GetPlatformDBSavedGamesPath('HCDB-160414', 'Sc1')));
  AssertTrue('AutoSaves dir exists',
    DirectoryExists(FOrg.GetPlatformDBAutoSavesPath('HCDB-160414', 'Sc1')));
end;

{ --------------------------------------------------------------------------- }
{ Listing                                                                      }
{ --------------------------------------------------------------------------- }

procedure TTestFileOrganization.TestListSubdirectories_Empty;
var
  NonExistent: String;
  Result: TStringArray;
begin
  NonExistent := FTempDir + PathDelim + 'does_not_exist';
  Result := FOrg.ListSubdirectories(NonExistent);
  AssertEquals('Empty listing for nonexistent dir', 0, Length(Result));
end;

procedure TTestFileOrganization.TestListSubdirectories;
var
  Base: String;
  Names: TStringArray;
  I: Integer;
  Found: array[0..2] of Boolean;
begin
  Base := FTempDir + PathDelim + 'ListTest';
  ForceDirectories(Base + PathDelim + 'Alpha');
  ForceDirectories(Base + PathDelim + 'Beta');
  ForceDirectories(Base + PathDelim + 'Gamma');
  // Also create a file — it must NOT appear in the listing.
  TouchFile(Base + PathDelim + 'file.txt');

  Names := FOrg.ListSubdirectories(Base);

  AssertEquals('Expected 3 subdirectories', 3, Length(Names));

  Found[0] := False; Found[1] := False; Found[2] := False;
  for I := 0 to High(Names) do
  begin
    if Names[I] = 'Alpha' then Found[0] := True;
    if Names[I] = 'Beta'  then Found[1] := True;
    if Names[I] = 'Gamma' then Found[2] := True;
  end;
  AssertTrue('Alpha listed',  Found[0]);
  AssertTrue('Beta listed',   Found[1]);
  AssertTrue('Gamma listed',  Found[2]);
end;

procedure TTestFileOrganization.TestListFiles_Empty;
var
  NonExistent: String;
  Result: TStringArray;
begin
  NonExistent := FTempDir + PathDelim + 'no_such_dir';
  Result := FOrg.ListFiles(NonExistent, '');
  AssertEquals('Empty listing for nonexistent dir', 0, Length(Result));
end;

procedure TTestFileOrganization.TestListFiles_WithExtFilter;
var
  Base: String;
  Files: TStringArray;
  I: Integer;
  FoundRes: Boolean;
begin
  Base := FTempDir + PathDelim + 'FileTest';
  ForceDirectories(Base);
  TouchFile(Base + PathDelim + 'battle.res');
  TouchFile(Base + PathDelim + 'platform.res');
  TouchFile(Base + PathDelim + 'readme.txt');
  TouchFile(Base + PathDelim + 'notes.doc');

  Files := FOrg.ListFiles(Base, '.res');

  AssertEquals('Only .res files returned', 2, Length(Files));
  FoundRes := True;
  for I := 0 to High(Files) do
    if ExtractFileExt(Files[I]) <> '.res' then
    begin
      FoundRes := False;
      Break;
    end;
  AssertTrue('All returned files have .res extension', FoundRes);
end;

procedure TTestFileOrganization.TestListFiles_WildcardFilter;
var
  Base: String;
  Files: TStringArray;
  I: Integer;
  Found_scs, Found_scq, Found_scxx: Boolean;
begin
  Base := FTempDir + PathDelim + 'WildTest';
  ForceDirectories(Base);
  TouchFile(Base + PathDelim + 'scen1.scs');   // must match .sc?
  TouchFile(Base + PathDelim + 'scen2.scq');   // must match .sc?
  TouchFile(Base + PathDelim + 'scen3.scxx');  // must NOT match .sc?
  TouchFile(Base + PathDelim + 'readme.txt');  // must NOT match .sc?

  Files := FOrg.ListFiles(Base, '.sc?');

  Found_scs  := False;
  Found_scq  := False;
  Found_scxx := False;
  for I := 0 to High(Files) do
  begin
    if ExtractFileExt(Files[I]) = '.scs'  then Found_scs  := True;
    if ExtractFileExt(Files[I]) = '.scq'  then Found_scq  := True;
    if ExtractFileExt(Files[I]) = '.scxx' then Found_scxx := True;
  end;

  AssertTrue( '.scs file matched by .sc?',           Found_scs);
  AssertTrue( '.scq file matched by .sc?',           Found_scq);
  AssertFalse('.scxx file must NOT match .sc?',      Found_scxx);
  AssertEquals('Exactly 2 files matched .sc?', 2, Length(Files));
end;

{ --------------------------------------------------------------------------- }
{ Edge cases                                                                   }
{ --------------------------------------------------------------------------- }

procedure TTestFileOrganization.TestBaseDirTrailingPathDelim;
var
  OrgWithSlash: IHCLFileOrganization;
  ExpectedRoot, ActualRoot: String;
begin
  OrgWithSlash := CreateFileOrganization(FTempDir + PathDelim);

  ExpectedRoot := FTempDir + PathDelim + HCL_MANAGED_DIR;
  ActualRoot   := OrgWithSlash.GetManagedRoot;

  AssertEquals('No double delimiter with trailing basedir slash',
    ExpectedRoot, ActualRoot);

  // Verify the double-delimiter is absent
  AssertEquals('Path does not contain double delimiter',
    0, Pos(PathDelim + PathDelim, ActualRoot));
end;

procedure TTestFileOrganization.TestNamesWithSpaces;
const
  BS   = 'Med Crisis';
  SCEN = 'Day 1 Strike';
begin
  AssertEquals('BattleSet path with spaces',
    CatPath(HCL_CATEGORY_NAMES[hclBattleSets]) + PathDelim + BS,
    FOrg.GetBattleSetPath(BS));

  AssertEquals('BattleSet scenarios path with spaces',
    CatPath(HCL_CATEGORY_NAMES[hclBattleSets]) + PathDelim + BS
      + PathDelim + HCL_SCENARIOS_DIR,
    FOrg.GetBattleSetScenariosPath(BS));

  AssertEquals('BattleSet scenario path with spaces',
    CatPath(HCL_CATEGORY_NAMES[hclBattleSets]) + PathDelim + BS
      + PathDelim + HCL_SCENARIOS_DIR + PathDelim + SCEN,
    FOrg.GetBattleSetScenarioPath(BS, SCEN));

  AssertEquals('BattleSet saved games path with spaces',
    CatPath(HCL_CATEGORY_NAMES[hclBattleSets]) + PathDelim + BS
      + PathDelim + HCL_SCENARIOS_DIR + PathDelim + SCEN
      + PathDelim + HCL_SAVEDGAMES_DIR,
    FOrg.GetBattleSetSavedGamesPath(BS, SCEN));

  AssertEquals('BattleSet auto-saves path with spaces',
    CatPath(HCL_CATEGORY_NAMES[hclBattleSets]) + PathDelim + BS
      + PathDelim + HCL_SCENARIOS_DIR + PathDelim + SCEN
      + PathDelim + HCL_SAVEDGAMES_DIR + PathDelim + HCL_AUTOSAVES_DIR,
    FOrg.GetBattleSetAutoSavesPath(BS, SCEN));
end;

initialization
  RegisterTest(TTestFileOrganization);

end.
