unit Tests.Myc.DirectoryMonitor;

interface

uses
    DUnitX.TestFramework,
    System.SysUtils,
    System.IOUtils,
    System.Classes,
    System.Generics.Collections,
    System.SyncObjs,
    Myc.DirectoryMonitor,
    Myc.TaskManager,
    Myc.Signals;

type
    [TestFixture]
    TTestDirectoryMonitor = class(TObject)
    private
        // Class fields for paths, shared across all tests in the fixture
        class var
            FTestDir: string;
            FSubDir1: string;
            FMultiDir1: string;
            FMultiDir1a: string;
            FMultiDir2: string;
            FMultiDir2a: string;

            FMonitorFactory: IDirectoryMonitorFactory;

        procedure CreateEmptyFile(const AFileName: string);
        class procedure CleanDirectoryContent(const Path: string);
        class procedure ForceDeleteDirectory(const Path: string);
    public
        [SetupFixture]
        class procedure FixtureSetup;
        [TearDownFixture]
        class procedure FixtureTearDown;

        [Setup]
        procedure Setup;
        [TearDown]
        procedure TearDown;

        [Test]
        procedure TestInitialization_Finds_Correct_Initial_Files;
        [Test]
        [IgnoreMemoryLeaks]
        procedure TestFile_Triggers_Changed_Notification;
        [Test]
        procedure TestMultiDirectory_MultiExtension_And_Subdirs;
        [Test]
        [IgnoreMemoryLeaks]
        procedure TestFile_Rename_And_Move_Triggers_Correct_Notifications;
        [Test]
        procedure TestRapid_CreateDelete_IsHandledCorrectly;
        [Test]
        [Timeout(30000)] // 30 second timeout for this potentially slow test
        procedure TestStress_BufferOverflowRecovery;
    end;

implementation

{ TTestDirectoryMonitor }

class procedure TTestDirectoryMonitor.FixtureSetup;
begin
    // This runs once before any tests in this class.
    // Initialize all paths
    //    FTestDir := TPath.Combine(TPath.GetTempPath, 'DirectoryMonitorTest');
    FTestDir := TPath.Combine('\\COFFEE\TickData\Test', 'DirectoryMonitorTest');

    FSubDir1 := TPath.Combine(FTestDir, 'Sub1');
    FMultiDir1 := TPath.Combine(FTestDir, 'Dir1');
    FMultiDir1a := TPath.Combine(FMultiDir1, 'Sub1A');
    FMultiDir2 := TPath.Combine(FTestDir, 'Dir2');
    FMultiDir2a := TPath.Combine(FMultiDir2, 'Sub2A');

    // Ensure a clean state from previous runs
    ForceDeleteDirectory(FTestDir);
    Sleep(200); // Give network file system time to process deletion

    // Create the entire directory structure needed for all tests
    TDirectory.CreateDirectory(FTestDir);
    TDirectory.CreateDirectory(FSubDir1);
    TDirectory.CreateDirectory(FMultiDir1);
    TDirectory.CreateDirectory(FMultiDir1a);
    TDirectory.CreateDirectory(FMultiDir2);
    TDirectory.CreateDirectory(FMultiDir2a);
end;

class procedure TTestDirectoryMonitor.FixtureTearDown;
begin
end;

procedure TTestDirectoryMonitor.Setup;
begin
    // This runs before each individual test.
    // Clean files from previous test, but leave directory structure intact.
    CleanDirectoryContent(FTestDir);

    // Get a fresh factory instance for each test.
    FMonitorFactory := CreateDirectoryMonitorFactory;
end;

procedure TTestDirectoryMonitor.TearDown;
begin
    // This runs after each individual test.
    // Release the factory and the monitor it created.
    FMonitorFactory := nil;
end;

class procedure TTestDirectoryMonitor.CleanDirectoryContent(const Path: string);
begin
    // Helper to delete all files and recursively clean subdirectories,
    // leaving the directory structure itself in place.
    if not TDirectory.Exists(Path) then
        exit;

    for var dir in TDirectory.GetDirectories(Path) do
        CleanDirectoryContent(dir);

    for var f in TDirectory.GetFiles(Path) do
        TFile.Delete(f);
end;

class procedure TTestDirectoryMonitor.ForceDeleteDirectory(const Path: string);
begin
    if not TDirectory.Exists(Path) then
        exit;

    CleanDirectoryContent(Path);

    // Finally, delete the now-empty directory
    TDirectory.Delete(Path, false);
end;

procedure TTestDirectoryMonitor.CreateEmptyFile(const AFileName: string);
begin
    TFile.WriteAllText(AFileName, '', TEncoding.UTF8);
end;

procedure TTestDirectoryMonitor.TestInitialization_Finds_Correct_Initial_Files;
var
    monitor: IDirectoryMonitor;
    file1_txt: string;
    file2_log: string;
    file3_sub_txt: string;
begin
    // Arrange: The directories exist; we only need to create the files.
    file1_txt := TPath.Combine(FTestDir, 'file1.txt');
    file2_log := TPath.Combine(FTestDir, 'file2.log');
    file3_sub_txt := TPath.Combine(FSubDir1, 'file3.txt');

    CreateEmptyFile(file1_txt);
    CreateEmptyFile(file2_log);
    CreateEmptyFile(file3_sub_txt);

    FMonitorFactory.AddDirectory(FTestDir, TSubDirectoryHandling.Recursive);
    FMonitorFactory.AddExtension('.txt');
    FMonitorFactory.SetChangeKinds([TFileChangeKind.FileNameChange]);

    // Act
    monitor := FMonitorFactory.CreateMonitor;

    // Assert
    Assert.AreEqual(2, monitor.FileCount, 'Should find exactly 2 .txt files');
    Assert.IsNotNull(monitor.Files[file1_txt], 'file1.txt should be found.');
    Assert.IsNotNull(monitor.Files[file3_sub_txt], 'file3.txt should be found.');
    Assert.IsNull(monitor.Files[file2_log], 'file2.log should be ignored.');
end;

procedure TTestDirectoryMonitor.TestFile_Triggers_Changed_Notification;
var
    monitor: IDirectoryMonitor;
    fileToChange: string;
    initialFileInfo: IFileInfo;
    notificationReceived: System.SyncObjs.TEvent;
    notifiedState: TFileState;
    receivedFileInfo: IFileInfo;
    waitResult: TWaitResult;
begin
    // Arrange
    fileToChange := TPath.Combine(FTestDir, 'change_me.txt');
    CreateEmptyFile(fileToChange);

    FMonitorFactory.AddDirectory(FTestDir, TSubDirectoryHandling.Ignore);
    FMonitorFactory.AddExtension('.txt');
    FMonitorFactory.SetChangeKinds([TFileChangeKind.LastWriteChange, TFileChangeKind.SizeChange]);

    monitor := FMonitorFactory.CreateMonitor;
    initialFileInfo := monitor.Files[fileToChange];
    Assert.IsNotNull(initialFileInfo, 'File to change should be present after initialization.');

    notificationReceived := System.SyncObjs.TEvent.Create(nil, true, false, '');
    try
        notifiedState := TFileState.Unchanged;
        receivedFileInfo := nil;

        initialFileInfo.AddChangeNotification(
            function(AFileInfo: IFileInfo; ANewState: TFileState): Boolean
            begin
                receivedFileInfo := AFileInfo;
                notifiedState := ANewState;
                notificationReceived.SetEvent;
                Result := false;
            end
        );

        // Act
        TFile.AppendAllText(fileToChange, 'some new content');

        // Assert
        waitResult := notificationReceived.WaitFor(2000);

        Assert.AreEqual(TWaitResult.wrSignaled, waitResult, 'Notification event was not signaled in time.');
        Assert.AreEqual(TFileState.Changed, notifiedState, 'Notification state should be "Changed".');
    finally
        notificationReceived.Free;
    end;
end;

procedure TTestDirectoryMonitor.TestMultiDirectory_MultiExtension_And_Subdirs;
var
    monitor: IDirectoryMonitor;
    f1_log, f3_tmp, f5_log: string;
    f2_txt, f6_ignored_tmp: string;
begin
    // Arrange: Directories are pre-created. We just create files.
    f1_log := TPath.Combine(FMultiDir1, 'file1.log');
    f2_txt := TPath.Combine(FMultiDir1, 'file2.txt');
    f3_tmp := TPath.Combine(FMultiDir1a, 'file3.tmp');
    f5_log := TPath.Combine(FMultiDir2, 'file5.log');
    f6_ignored_tmp := TPath.Combine(FMultiDir2a, 'file6.tmp');

    CreateEmptyFile(f1_log);
    CreateEmptyFile(f2_txt);
    CreateEmptyFile(f3_tmp);
    CreateEmptyFile(f5_log);
    CreateEmptyFile(f6_ignored_tmp);

    FMonitorFactory.AddDirectory(FMultiDir1, TSubDirectoryHandling.Recursive);
    FMonitorFactory.AddDirectory(FMultiDir2, TSubDirectoryHandling.Ignore);
    FMonitorFactory.AddExtension('.log');
    FMonitorFactory.AddExtension('.tmp');
    FMonitorFactory.SetChangeKinds([TFileChangeKind.FileNameChange]);

    // Act
    monitor := FMonitorFactory.CreateMonitor;

    // Assert
    Assert.AreEqual(3, monitor.FileCount, 'Should find 3 files (.log or .tmp) respecting recursion rules.');
    Assert.IsNotNull(monitor.Files[f1_log], 'f1.log should be found in Dir1.');
    Assert.IsNotNull(monitor.Files[f3_tmp], 'f3.tmp should be found in recursive Dir1/Sub1A.');
    Assert.IsNotNull(monitor.Files[f5_log], 'f5.log should be found in Dir2.');
    Assert.IsNull(monitor.Files[f2_txt], 'f2.txt should be ignored due to extension mismatch.');
    Assert.IsNull(monitor.Files[f6_ignored_tmp], 'f6.tmp should be ignored because Dir2 is not monitored recursively.');
end;

procedure TTestDirectoryMonitor.TestFile_Rename_And_Move_Triggers_Correct_Notifications;
var
    monitor: IDirectoryMonitor;
    originalFile, renamedFile, movedFile: string;
    countdown: TCountdownEvent;
    initialInfo: IFileInfo;
    waitResult: TWaitResult;
begin
    // Arrange
    originalFile := TPath.Combine(FTestDir, 'original.txt');
    renamedFile := TPath.Combine(FTestDir, 'renamed.txt');
    movedFile := TPath.Combine(FSubDir1, 'moved.txt');
    CreateEmptyFile(originalFile);

    FMonitorFactory.AddDirectory(FTestDir, TSubDirectoryHandling.Recursive);
    FMonitorFactory.AddExtension('.txt');
    FMonitorFactory.SetChangeKinds([TFileChangeKind.FileNameChange]);
    monitor := FMonitorFactory.CreateMonitor;

    countdown := TCountdownEvent.Create(1);
    try
        initialInfo := monitor.Files[originalFile];
        Assert.IsNotNull(initialInfo, 'Initial file must exist in monitor.');
        initialInfo.AddChangeNotification(
            function(AFileInfo: IFileInfo; ANewState: TFileState): Boolean
            begin
                if ANewState = TFileState.Deleted then
                    countdown.Signal;
                Result := true;
            end
        );

        // Act & Assert 1: Rename
        TFile.Move(originalFile, renamedFile);
        waitResult := countdown.WaitFor(2000);
        Assert.AreEqual(TWaitResult.wrSignaled, waitResult, 'Delete notification for original file did not arrive in time.');
        Sleep(100);
        Assert.IsNotNull(monitor.Files[renamedFile], 'Renamed file should be present in monitor.');

        // Arrange 2
        countdown.Reset(1);
        var renamedInfo := monitor.Files[renamedFile];
        Assert.IsNotNull(renamedInfo);
        renamedInfo.AddChangeNotification(
            function(AFileInfo: IFileInfo; ANewState: TFileState): Boolean
            begin
                if ANewState = TFileState.Deleted then
                    countdown.Signal;
                Result := true;
            end
        );

        // Act & Assert 2: Move
        TFile.Move(renamedFile, movedFile);
        waitResult := countdown.WaitFor(2000);
        Assert.AreEqual(TWaitResult.wrSignaled, waitResult, 'Delete notification for renamed file did not arrive in time.');
        Sleep(100);
        Assert.IsNotNull(monitor.Files[movedFile], 'Moved file should be present in monitor.');
        Assert.AreEqual(1, monitor.FileCount, 'Monitor should only contain one file at the end.');
    finally
        countdown.Free;
    end;
end;

procedure TTestDirectoryMonitor.TestRapid_CreateDelete_IsHandledCorrectly;
var
    monitor: IDirectoryMonitor;
    rapidFile: string;
begin
    // Arrange
    rapidFile := TPath.Combine(FTestDir, 'rapid.txt');
    FMonitorFactory.AddDirectory(FTestDir, TSubDirectoryHandling.Ignore);
    FMonitorFactory.AddExtension('.txt');
    FMonitorFactory.SetChangeKinds([TFileChangeKind.FileNameChange]);
    monitor := FMonitorFactory.CreateMonitor;

    // Act
    CreateEmptyFile(rapidFile);
    TFile.Delete(rapidFile);
    Sleep(500);

    // Assert
    Assert.AreEqual(0, monitor.FileCount, 'Monitor should be empty after rapid create/delete.');
    Assert.IsNull(monitor.Files[rapidFile], 'The rapidly created/deleted file should not exist in the monitor.');
end;

procedure TTestDirectoryMonitor.TestStress_BufferOverflowRecovery;
const
    NumFiles = 4000;
var
    monitor: IDirectoryMonitor;
    i: Integer;
    filePath: string;
begin
    // Arrange
    FMonitorFactory.AddDirectory(FTestDir, TSubDirectoryHandling.Ignore);
    FMonitorFactory.AddExtension('.tmp');
    FMonitorFactory.SetChangeKinds([TFileChangeKind.FileNameChange]);
    monitor := FMonitorFactory.CreateMonitor;

    // Act
    for i := 1 to NumFiles do
    begin
        filePath := TPath.Combine(FTestDir, IntToStr(i) + '.tmp');
        CreateEmptyFile(filePath);
    end;
    Sleep(5000);

    // Assert
    Assert.AreEqual(NumFiles, monitor.FileCount, 'Monitor should recover from overflow and report the correct file count.');
end;

end.
