#region Apache License

//
// Licensed to the Apache Software Foundation (ASF) under one or more 
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership. 
// The ASF licenses this file to you under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with 
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using log4net.Appender;
using log4net.Core;
using log4net.Layout;
using log4net.Repository.Hierarchy;
using log4net.Util;
using NUnit.Framework;
using System.Globalization;
using System.Linq;

namespace log4net.Tests.Appender;

/// <summary>
/// Used for internal unit testing the <see cref="RollingFileAppender"/> class.
/// </summary>
[TestFixture]
public class RollingFileAppenderTest
{
  protected string FileName = "test_41d3d834_4320f4da.log";

  private const string TestMessage98Chars =
    "01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567";

  private const string TestMessage99Chars =
    "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678";

  private const int MaximumFileSize = 450; // in bytes
  private int _messagesLogged;
  private int _countDirection;
  private int _maxSizeRollBackups = 3;
  private CountingAppender? _caRoot;
  private Logger? _root;
  private CultureInfo? _currentCulture;
  private CultureInfo? _currentUiCulture;

  private sealed class SilentErrorHandler : IErrorHandler
  {
    private readonly StringBuilder _buffer = new();

    public string Message => _buffer.ToString();

    public void Error(string message)
      => _buffer.Append(message + '\n');

    public void Error(string message, Exception e)
      => _buffer.Append(message + '\n' + e.Message + '\n');

    public void Error(string message, Exception? e, ErrorCode errorCode)
      => _buffer.Append(message + '\n' + e?.Message + '\n');
  }

  private sealed class RollingFileAppenderForTest : RollingFileAppender
  {
    /// <summary>
    /// Builds a list of filenames for all files matching the base filename plus a file pattern.
    /// </summary>
    internal new List<string> GetExistingFiles(string baseFilePath)
      => base.GetExistingFiles(baseFilePath);
  }

  /// <summary>
  /// Sets up variables used for the tests
  /// </summary>
  private void InitializeVariables()
  {
    _messagesLogged = 0;
    _countDirection = +1; // Up
    _maxSizeRollBackups = 3;
  }

  /// <summary>
  /// Shuts down any loggers in the hierarchy, along
  /// with all appenders, and deletes any test files used
  /// for logging.
  /// </summary>
  private void ResetAndDeleteTestFiles()
  {
    // Regular users should not use the clear method lightly!
    LogManager.GetRepository().ResetConfiguration();
    LogManager.GetRepository().Shutdown();
    ((Repository.Hierarchy.Hierarchy)LogManager.GetRepository()).Clear();

    DeleteTestFiles();
  }

  /// <summary>
  /// Any initialization that happens before each test can
  /// go here
  /// </summary>
  [SetUp]
  public void SetUp()
  {
    ResetAndDeleteTestFiles();
    InitializeVariables();

    // set correct thread culture
    _currentCulture = System.Threading.Thread.CurrentThread.CurrentCulture;
    _currentUiCulture = System.Threading.Thread.CurrentThread.CurrentUICulture;
    System.Threading.Thread.CurrentThread.CurrentCulture =
        System.Threading.Thread.CurrentThread.CurrentUICulture =
            CultureInfo.InvariantCulture;
  }

  /// <summary>
  /// Any steps that happen after each test go here
  /// </summary>
  [TearDown]
  public void TearDown()
  {
    ResetAndDeleteTestFiles();

    // restore previous culture
    System.Threading.Thread.CurrentThread.CurrentCulture = _currentCulture!;
    System.Threading.Thread.CurrentThread.CurrentUICulture = _currentUiCulture!;
  }

  /// <summary>
  /// Finds the number of files that match the base file name,
  /// and matches the result against an expected count
  /// </summary>
  private void VerifyFileCount(int expectedCount, bool preserveLogFileNameExtension = false)
  {
    List<string> files = GetExistingFiles(FileName, preserveLogFileNameExtension);
    Assert.That(files, Is.Not.Null);
    Assert.That(files, Has.Count.EqualTo(expectedCount));
  }

  /// <summary>
  /// Creates a file with the given number, and the shared base file name
  /// </summary>
  private void CreateFile(int fileNumber)
  {
    FileInfo fileInfo = new(MakeFileName(FileName, fileNumber));

    FileStream? fileStream = null;
    try
    {
      fileStream = fileInfo.Create();
    }
    finally
    {
      if (null != fileStream)
      {
        try
        {
          fileStream.Close();
        }
        catch (ObjectDisposedException)
        {
          // Ignore
        }
      }
    }
  }

  /// <summary>
  /// Verifies that the code correctly loads all filenames
  /// </summary>
  [Test]
  public void TestGetExistingFiles()
  {
    VerifyFileCount(0);
    CreateFile(0);
    VerifyFileCount(1);
    CreateFile(1);
    VerifyFileCount(2);
  }

  [Test]
  public void RollingCombinedWithPreserveExtension()
  {
    _root = ((Repository.Hierarchy.Hierarchy)LogManager.GetRepository()).Root;
    _root.Level = Level.All;
    PatternLayout patternLayout = new();
    patternLayout.ActivateOptions();

    RollingFileAppender roller = new()
    {
      StaticLogFileName = false,
      Layout = patternLayout,
      AppendToFile = true,
      RollingStyle = RollingFileAppender.RollingMode.Composite,
      DatePattern = "dd_MM_yyyy",
      MaxSizeRollBackups = 1,
      CountDirection = 1,
      PreserveLogFileNameExtension = true,
      MaximumFileSize = "10KB",
      File = FileName
    };
    roller.ActivateOptions();
    _root.AddAppender(roller);

    _root.Repository!.Configured = true;

    for (int i = 0; i < 1000; i++)
    {
      StringBuilder s = new();
      for (int j = 50; j < 100; j++)
      {
        if (j > 50)
        {
          s.Append(' ');
        }

        s.Append(j);
      }

      _root.Log(Level.Debug, s.ToString(), null);
    }

    VerifyFileCount(2, true);
  }

  /// <summary>
  /// Removes all test files that exist
  /// </summary>
  private void DeleteTestFiles()
  {
    List<string> files = GetExistingFiles(FileName);
    files.AddRange(GetExistingFiles(FileName, true));
    foreach (string file in files)
    {
      try
      {
        Debug.WriteLine("Deleting test file " + file);
        File.Delete(file);
      }
      catch (Exception e) when (e is not null)
      {
        Debug.WriteLine("Exception while deleting test file " + e);
      }
    }
  }

  /// <summary>
  /// Generates a file name associated with the count, using
  /// the base file name.
  /// </summary>
  /// <param name="baseFile"></param>
  /// <param name="fileCount"></param>
  /// <returns></returns>
  private static string MakeFileName(string baseFile, int fileCount)
  {
    if (0 == fileCount)
    {
      return baseFile;
    }

    return baseFile + "." + fileCount;
  }

  /// <summary>
  /// Returns a RollingFileAppender using all the internal settings for maximum
  /// file size and number of backups
  /// </summary>
  /// <returns></returns>
  private RollingFileAppender CreateAppender() => CreateAppender(new FileAppender.ExclusiveLock());

  /// <summary>
  /// Returns a RollingFileAppender using all the internal settings for maximum
  /// file size and number of backups
  /// </summary>
  /// <param name="lockModel">The locking model to test</param>
  /// <returns></returns>
  private RollingFileAppender CreateAppender(FileAppender.LockingModelBase lockModel)
  {
    //
    // Use a basic pattern that
    // includes just the message and a CR/LF.
    //
    PatternLayout layout = new("%m%n");

    //
    // Create the new appender
    //
    RollingFileAppender appender = new()
    {
      Layout = layout,
      File = FileName,
      Encoding = Encoding.ASCII,
      MaximumFileSize = MaximumFileSize.ToString(),
      MaxSizeRollBackups = _maxSizeRollBackups,
      CountDirection = _countDirection,
      RollingStyle = RollingFileAppender.RollingMode.Size,
      LockingModel = lockModel
    };

    appender.ActivateOptions();

    return appender;
  }

  /// <summary>
  /// Used for test purposes, a table of these objects can be used to identify
  /// any existing files and their expected length.
  /// </summary>
  private sealed record RollFileEntry(string FileName, long FileLength);

  /// <summary>
  /// Used for table-driven testing.  This class holds information that can be used
  /// for testing of file rolling.
  /// </summary>
  /// <param name="PreLogFileEntries">
  /// A table of entries showing files that should exist and their expected sizes
  /// before logging is called
  /// </param>
  /// <param name="PostLogFileEntries">
  /// A table of entries showing files that should exist and their expected sizes
  /// after a message is logged
  /// </param>
  private sealed record RollConditions(IList<RollFileEntry> PreLogFileEntries, IList<RollFileEntry> PostLogFileEntries);

  private static void VerifyExistenceAndRemoveFromList(List<string> existing,
    string fileName, FileInfo file, RollFileEntry entry)
  {
    Assert.That(existing, Does.Contain(fileName), $"filename {fileName} not found in test directory");
    Assert.That(file.Length, Is.EqualTo(entry.FileLength), "file length mismatch");
    // Remove this file from the list
    existing.Remove(fileName);
  }

  /// <summary>
  /// Checks that all the expected files exist, and only the expected files.  Also
  /// verifies the length of all files against the expected length
  /// </summary>
  /// <param name="baseFileName"></param>
  /// <param name="fileEntries"></param>
  private static void VerifyFileConditions(string baseFileName, IList<RollFileEntry> fileEntries)
  {
    List<string> existingFiles = GetExistingFiles(baseFileName);

    foreach (RollFileEntry rollFile in fileEntries)
    {
      string fileName = rollFile.FileName;
      Assert.That(fileName, Is.Not.Null);
      FileInfo file = new(fileName);

      if (rollFile.FileLength > 0)
      {
        Assert.That(file.Exists, $"file {fileName} does not exist");
        VerifyExistenceAndRemoveFromList(existingFiles, fileName, file, rollFile);
      }
      else
      {
        // If length is 0, file may not exist yet.  If file exists, make sure length
        // is zero.  If file doesn't exist, this is OK

        if (file.Exists)
        {
          VerifyExistenceAndRemoveFromList(existingFiles, fileName, file, rollFile);
        }
      }
    }

    // This check ensures no extra files matching the wildcard pattern exist.
    // We only want the files we expect, and no others
    Assert.That(existingFiles, Is.Empty);
  }

  /// <summary>
  /// Called before logging a message to check that all the expected files exist, 
  /// and only the expected files.  Also verifies the length of all files against 
  /// the expected length
  /// </summary>
  /// <param name="baseFileName"></param>
  /// <param name="entry"></param>
  private static void VerifyPreConditions(string baseFileName, RollConditions entry)
    => VerifyFileConditions(baseFileName, entry.PreLogFileEntries);

  /// <summary>
  /// Called after logging a message to check that all the expected files exist, 
  /// and only the expected files.  Also verifies the length of all files against 
  /// the expected length
  /// </summary>
  /// <param name="baseFileName"></param>
  /// <param name="entry"></param>
  private static void VerifyPostConditions(string baseFileName, RollConditions entry)
    => VerifyFileConditions(baseFileName, entry.PostLogFileEntries);

  /// <summary>
  /// Logs a message, verifying the expected message counts against the 
  /// current running totals.
  /// </summary>
  private void LogMessage(string messageToLog)
  {
    Assert.That(_caRoot, Is.Not.Null);
    Assert.That(_messagesLogged++, Is.EqualTo(_caRoot.Counter));
    Assert.That(_root, Is.Not.Null);
    _root.Log(Level.Debug, messageToLog, null);
    Assert.That(_messagesLogged, Is.EqualTo(_caRoot.Counter));
  }

  /// <summary>
  /// Runs through all table entries, logging messages.  Before each message is logged,
  /// pre-conditions are checked to ensure the expected files exists, and they are the
  /// expected size.  After logging, verifies the same.
  /// </summary>
  /// <param name="baseFileName"></param>
  /// <param name="entries"></param>
  /// <param name="messageToLog"></param>
  private void RollFromTableEntries(string baseFileName, RollConditions[] entries, string messageToLog)
  {
    foreach (RollConditions entry in entries)
    {
      VerifyPreConditions(baseFileName, entry);
      LogMessage(messageToLog);
      VerifyPostConditions(baseFileName, entry);
    }
  }

  private static readonly int _sNewlineLength = Environment.NewLine.Length;

  /// <summary>
  /// Returns the number of bytes logged per message, including
  /// any newline characters in addition to the message length.
  /// </summary>
  /// <returns></returns>
  private static int TotalMessageLength(string message) => message.Length + _sNewlineLength;

  /// <summary>
  /// Determines how many messages of a fixed length can be logged
  /// to a single file before the file rolls.
  /// </summary>
  /// <returns></returns>
  private static int MessagesPerFile(int messageLength)
  {
    int messagesPerFile = MaximumFileSize / messageLength;

    //
    // RollingFileAppender checks for wrap BEFORE logging,
    // so we will actually get one more message per file than
    // we would otherwise.
    //
    if (messagesPerFile * messageLength < MaximumFileSize)
    {
      messagesPerFile++;
    }

    return messagesPerFile;
  }

  /// <summary>
  /// Current file name is always the base file name when counting. Dates will need a different approach.
  /// </summary>
  /// <returns></returns>
  private string GetCurrentFile() => FileName;

  /// <summary>
  /// Turns a group of file names into an array of file entries that include the name
  /// and a size.  This is useful for assigning the properties of backup files, when
  /// the length of the files are all the same size due to a fixed message length.
  /// </summary>
  /// <returns></returns>
  private static List<RollFileEntry> MakeBackupFileEntriesFromBackupGroup(
    string backupGroup, 
    int backupFileLength)
  {
    // ReSharper disable once ArrangeMethodOrOperatorBody
    return backupGroup.Split(' ')
      .Where(file => file.Trim().Length > 0)
      .Select(file => new RollFileEntry(file, backupFileLength))
      .ToList();
  }

  /// <summary>
  /// Finds the iGroup group in the string (comma separated groups)
  /// </summary>
  /// <returns></returns>
  private static string GetBackupGroup(string backupGroups, int group) 
    => backupGroups.Split(',')[group];

  /// <summary>
  /// Builds a collection of file entries based on the file names
  /// specified in a groups string and the max file size from the
  /// stats object
  /// </summary>
  /// <returns></returns>
  private static List<RollFileEntry>? MakeBackupFileEntriesForPostCondition(string backupGroups, RollingStats stats)
  {
    if (0 == stats.NumberOfFileRolls)
    {
      return null; // first round has no previous backups
    }

    string group = GetBackupGroup(backupGroups, stats.NumberOfFileRolls - 1);
    return MakeBackupFileEntriesFromBackupGroup(group, stats.MaximumFileSize);
  }

  /// <summary>
  /// This class holds information that is used while we are generating
  /// test data sets
  /// </summary>
  /// <param name="TotalMessageLength">
  /// The length of a message, including any CR/LF characters.
  /// This length assumes all messages are a fixed length for test purposes.
  /// </param>
  /// <param name="MessagesPerFile">A count of the number of messages that are logged to each file.</param>
  private sealed record RollingStats(
    int TotalMessageLength,
    int MessagesPerFile)
  {
    /// <summary>
    /// Number of total bytes a log file can reach.
    /// </summary>
    // ReSharper disable once MemberHidesStaticFromOuterClass
    public int MaximumFileSize => TotalMessageLength * MessagesPerFile;

    /// <summary>
    /// Counts how many messages have been logged to the current file
    /// </summary>
    public int MessagesThisFile { get; set; }

    /// <summary>
    /// Counts how many times a file roll has occurred
    /// </summary>
    public int NumberOfFileRolls { get; set; }
  }

  /// <summary>
  /// The stats are used to keep track of progress while we are algorithmically
  /// generating a table of pre- / post-condition tests for file rolling.
  /// </summary>
  /// <param name="testMessage"></param>
  /// <returns></returns>
  private static RollingStats InitializeStats(string testMessage) 
    => new(TotalMessageLength(testMessage), MessagesPerFile(TotalMessageLength(testMessage)));

  /// <summary>
  /// Takes an existing array of RollFileEntry objects, creates a new array one element
  /// bigger, and appends the final element to the end.  If the existing entries are
  /// null (no entries), then a one-element array is returned with the final element
  /// as the only entry.
  /// </summary>
  /// <param name="existing"></param>
  /// <param name="final"></param>
  /// <returns></returns>
  private static List<RollFileEntry> AddFinalElement(List<RollFileEntry>? existing, RollFileEntry final)
  {
    int length = 1;
    if (existing is not null)
    {
      length += existing.Count;
    }

    List<RollFileEntry> combined = new(length);
    if (existing is not null)
    {
      combined.AddRange(existing);
    }

    combined.Add(final);
    return combined;
  }

  /// <summary>
  /// Generates the pre- and post-condition arrays from an array of backup files and the
  /// current file / next file.
  /// </summary>
  private static RollConditions BuildTableEntry(string backupFiles,
    RollConditions? preCondition,
    RollFileEntry current,
    RollFileEntry currentNext,
    RollingStats rollingStats)
  {
    List<RollFileEntry>? backupsPost = MakeBackupFileEntriesForPostCondition(backupFiles, rollingStats);
    List<RollFileEntry> post = AddFinalElement(backupsPost, currentNext);
    return preCondition is null 
      ? new(AddFinalElement(null, current), post) 
      : new(preCondition.PostLogFileEntries, post);
  }

  /// <summary>
  /// Returns a RollFileEntry that represents the next state of the current file,
  /// based on the current state.  When the current state would roll, the next
  /// entry is the current file wrapped to 0 bytes.  Otherwise, the next state
  /// is the post-condition passed in as the currentNext parameter
  /// </summary>
  /// <param name="rollingStats"></param>
  /// <param name="currentNext"></param>
  /// <returns></returns>
  private RollFileEntry MoveNextEntry(RollingStats rollingStats, RollFileEntry currentNext)
  {
    rollingStats.MessagesThisFile++;
    if (rollingStats.MessagesThisFile >= rollingStats.MessagesPerFile)
    {
      rollingStats.MessagesThisFile = 0;
      rollingStats.NumberOfFileRolls++;

      return new RollFileEntry(GetCurrentFile(), 0);
    }
    return currentNext;
  }

  /// <summary>
  /// Callback point for the regular expression parser.  Turns
  /// the number into a file name.
  /// </summary>
  private string NumberedNameMaker(Match match) 
    => MakeFileName(FileName, int.Parse(match.Value));

  /// <summary>
  /// Parses a numeric list of files, turning them into file names.
  /// Calls back to a method that does the actual replacement, turning
  /// the numeric value into a filename.
  /// </summary>
  [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "SYSLIB1045:Convert to 'GeneratedRegexAttribute'.", 
    Justification = "only .net8")]
  private static string ConvertToFiles(string backupInfo, MatchEvaluator evaluator) 
    => Regex.Replace(backupInfo, @"\d+", evaluator);

  /// <summary>
  /// Makes test entries used for verifying counted file names
  /// </summary>
  /// <param name="testMessage">A message to log repeatedly</param>
  /// <param name="backupInfo">Filename groups used to indicate backup file name progression
  /// that results after each message is logged</param>
  /// <param name="messagesToLog">How many times the test message will be repeatedly logged</param>
  /// <returns></returns>
  private RollConditions[] MakeNumericTestEntries(
    string testMessage, 
    string backupInfo,
    int messagesToLog)
    => MakeTestEntries(testMessage, backupInfo, messagesToLog, NumberedNameMaker);

  /// <summary>
  /// This routine takes a list of backup file names and a message that will be logged
  /// repeatedly, and generates a collection of objects containing pre-condition and 
  /// post-condition information.  This pre- / post-information shows the names and expected 
  /// file sizes for all files just before and just after a message is logged.
  /// </summary>
  /// <param name="testMessage">A message to log repeatedly</param>
  /// <param name="backupInfo">Filename groups used to indicate backup file name progression
  /// that results after each message is logged</param>
  /// <param name="messagesToLog">How many times the test message will be repeatedly logged</param>
  /// <param name="evaluator">Function that can turn a number into a filename</param>
  /// <returns></returns>
  private RollConditions[] MakeTestEntries(
    string testMessage,
    string backupInfo,
    int messagesToLog,
    MatchEvaluator evaluator)
  {
    string backupFiles = ConvertToFiles(backupInfo, evaluator);

    RollConditions[] table = new RollConditions[messagesToLog];

    RollingStats rollingStats = InitializeStats(testMessage);

    RollConditions? preCondition = null;
    rollingStats.MessagesThisFile = 0;

    RollFileEntry currentFile = new(GetCurrentFile(), 0);
    for (int i = 0; i < messagesToLog; i++)
    {
      RollFileEntry currentNext = new(
          GetCurrentFile(),
          (1 + rollingStats.MessagesThisFile) * rollingStats.TotalMessageLength);

      table[i] = BuildTableEntry(backupFiles, preCondition, currentFile, currentNext, rollingStats);
      preCondition = table[i];

      //System.Diagnostics.Debug.WriteLine( "Message " + i );
      //DumpTableEntry( table[i] );

      currentFile = MoveNextEntry(rollingStats, currentNext);
    }

    return table;
  }

  /// <summary>
  /// Uses the externally defined rolling table to verify rolling names/sizes
  /// </summary>
  /// <remarks>
  /// Pattern is:  check pre-conditions.  Log messages, checking size of current file.
  /// when size exceeds limit, check post conditions.  Can determine from message the
  /// number of messages N that will cause a roll to occur.  Challenge is to verify the
  /// expected files, their sizes, and the names.  For a message of length L, the backups
  /// will be of size (N * L), and the current file will be of size (K * L), where K is
  /// the number of messages that have been logged to this file.
  ///
  /// File sizes can be checked algorithmically.  
  /// 
  /// File names are generated using a table driven algorithm, where a number is turned into
  /// the actual filename.
  /// 
  /// The entries are comma-separated, with spaces between the names.  Each comma indicates
  /// a 'roll', and the group between commas indicates the numbers for all backup files that
  /// occur as a result of the roll.  It is assumed that no backup files exist before a roll 
  /// occurs
  /// </remarks>
  /// <param name="table"></param>
  private void VerifyRolling(RollConditions[] table)
  {
    ConfigureRootAppender();
    RollFromTableEntries(FileName, table, GetTestMessage());
  }

  /// <summary>
  /// Validates rolling using a fixed number of backup files, with
  /// count direction set to up, so that newer files have higher counts.
  /// Newest = N, Oldest = N-K, where K is the number of backups to allow
  /// and N is the number of times rolling has occurred.
  /// </summary>
  [Test]
  public void TestRollingCountUpFixedBackups()
  {
    //
    // Oldest to newest when reading in a group left-to-right, so 1 2 3 means 1 is the
    // oldest, and 3 is the newest
    //
    const string backupInfo = "1, 1 2, 1 2 3, 2 3 4, 3 4 5";

    //
    // Count Up
    //
    _countDirection = +1;

    //
    // Log 30 messages.  This is 5 groups, 6 checks per group (0, 100, 200, 300, 400, 500) 
    // bytes for current file as messages are logged.
    //
    const int messagesToLog = 30;

    VerifyRolling(MakeNumericTestEntries(GetTestMessage(), backupInfo, messagesToLog));
  }

  /// <summary>
  /// Validates rolling using an infinite number of backup files, with
  /// count direction set to up, so that newer files have higher counts.
  /// Newest = N, Oldest = 1, where N is the number of times rolling has 
  /// occurred.
  /// </summary>
  [Test]
  public void TestRollingCountUpInfiniteBackups()
  {
    //
    // Oldest to newest when reading in a group left-to-right, so 1 2 3 means 1 is the
    // oldest, and 3 is the newest
    //
    const string backupInfo = "1, 1 2, 1 2 3, 1 2 3 4, 1 2 3 4 5";

    //
    // Count Up
    //
    _countDirection = +1;

    //
    // Infinite backups
    //
    _maxSizeRollBackups = -1;

    //
    // Log 30 messages.  This is 5 groups, 6 checks per group (0, 100, 200, 300, 400, 500) 
    // bytes for current file as messages are logged.
    //
    const int messagesToLog = 30;

    VerifyRolling(MakeNumericTestEntries(GetTestMessage(), backupInfo, messagesToLog));
  }

  /// <summary>
  /// Validates rolling with no backup files, with count direction set to up.
  /// Only the current file should be present, wrapping to 0 bytes once the
  /// previous file fills up.
  /// </summary>
  [Test]
  public void TestRollingCountUpZeroBackups()
  {
    //
    // Oldest to newest when reading in a group left-to-right, so 1 2 3 means 1 is the
    // oldest, and 3 is the newest
    //
    const string backupInfo = ", , , , ";

    //
    // Count Up
    //
    _countDirection = +1;

    //
    // No backups
    //
    _maxSizeRollBackups = 0;

    //
    // Log 30 messages.  This is 5 groups, 6 checks per group (0, 100, 200, 300, 400, 500) 
    // bytes for current file as messages are logged.
    //
    const int messagesToLog = 30;

    VerifyRolling(MakeNumericTestEntries(GetTestMessage(), backupInfo, messagesToLog));
  }


  /// <summary>
  /// Validates rolling using a fixed number of backup files, with
  /// count direction set to down, so that older files have higher counts.
  /// Newest = 1, Oldest = N, where N is the number of backups to allow
  /// </summary>
  [Test]
  public void TestRollingCountDownFixedBackups()
  {
    //
    // Oldest to newest when reading in a group left-to-right, so 1 2 3 means 1 is the
    // oldest, and 3 is the newest
    //
    const string backupInfo = "1, 1 2, 1 2 3, 1 2 3, 1 2 3";

    //
    // Count Up
    //
    _countDirection = -1;

    //
    // Log 30 messages.  This is 5 groups, 6 checks per group (0, 100, 200, 300, 400, 500) 
    // bytes for current file as messages are logged.
    //
    const int messagesToLog = 30;

    VerifyRolling(MakeNumericTestEntries(GetTestMessage(), backupInfo, messagesToLog));
  }

  /// <summary>
  /// Validates rolling using an infinite number of backup files, with
  /// count direction set to down, so that older files have higher counts.
  /// Newest = 1, Oldest = N, where N is the number of times rolling has
  /// occurred
  /// </summary>
  [Test]
  public void TestRollingCountDownInfiniteBackups()
  {
    //
    // Oldest to newest when reading in a group left-to-right, so 1 2 3 means 1 is the
    // oldest, and 3 is the newest
    //
    const string backupInfo = "1, 1 2, 1 2 3, 1 2 3 4, 1 2 3 4 5";

    //
    // Count Down
    //
    _countDirection = -1;

    //
    // Infinite backups
    //
    _maxSizeRollBackups = -1;

    //
    // Log 30 messages.  This is 5 groups, 6 checks per group (0, 100, 200, 300, 400, 500) 
    // bytes for current file as messages are logged.
    //
    const int messagesToLog = 30;

    VerifyRolling(MakeNumericTestEntries(GetTestMessage(), backupInfo, messagesToLog));
  }

  /// <summary>
  /// Validates rolling with no backup files, with count direction set to down.
  /// Only the current file should be present, wrapping to 0 bytes once the
  /// previous file fills up.
  /// </summary>
  [Test]
  public void TestRollingCountDownZeroBackups()
  {
    //
    // Oldest to newest when reading in a group left-to-right, so 1 2 3 means 1 is the
    // oldest, and 3 is the newest
    //
    const string backupInfo = ", , , , ";

    //
    // Count Up
    //
    _countDirection = -1;

    //
    // No backups
    //
    _maxSizeRollBackups = 0;

    //
    // Log 30 messages.  This is 5 groups, 6 checks per group (0, 100, 200, 300, 400, 500) 
    // bytes for current file as messages are logged.
    //
    const int messagesToLog = 30;

    VerifyRolling(MakeNumericTestEntries(GetTestMessage(), backupInfo, messagesToLog));
  }

  /// <summary>
  /// Configures the root appender for counting and rolling
  /// </summary>
  private void ConfigureRootAppender()
  {
    _root = ((Repository.Hierarchy.Hierarchy)LogManager.GetRepository()).Root;
    _root.Level = Level.Debug;
    _caRoot = new CountingAppender();
    _root.AddAppender(_caRoot);
    Assert.That(_caRoot.Counter, Is.EqualTo(0));

    //
    // Set the root appender with a RollingFileAppender
    //
    _root.AddAppender(CreateAppender());

    if (_root.Repository is not null)
    {
      _root.Repository.Configured = true;
    }
  }

  /// <summary>
  /// Verifies that the current backup index is detected correctly when initializing
  /// </summary>
  /// <param name="baseFile"></param>
  /// <param name="files"></param>
  /// <param name="expectedCurSizeRollBackups"></param>
  private static void VerifyInitializeRollBackupsFromBaseFile(string baseFile,
    List<string> files, int expectedCurSizeRollBackups)
    => InitializeAndVerifyExpectedValue(files, baseFile, CreateRollingFileAppender("5,0,1"),
        expectedCurSizeRollBackups);

  /// <summary>
  /// Tests that the current backup index is 0 when no
  /// existing files are seen
  /// </summary>
  [Test]
  public void TestInitializeRollBackups1()
  {
    const string baseFile = "LogFile.log";
    List<string> files =
    [
      "junk1",
      "junk1.log",
      "junk2.log",
      "junk.log.1",
      "junk.log.2",
    ];

    const int expectedCurSizeRollBackups = 0;
    VerifyInitializeRollBackupsFromBaseFile(baseFile, files, expectedCurSizeRollBackups);
  }

  /// <summary>
  /// Verifies that files are detected when the base file is specified
  /// </summary>
  private static void VerifyInitializeRollBackupsFromBaseFile(string baseFile)
  {
    List<string> files = MakeTestDataFromString(baseFile, "0,1,2");

    const int expectedCurSizeRollBackups = 2;
    VerifyInitializeRollBackupsFromBaseFile(baseFile, files, expectedCurSizeRollBackups);
  }

  /// <summary>
  /// Verifies that count goes to the highest when counting up
  /// </summary>
  [Test]
  public void TestInitializeCountUpFixed()
  {
    List<string> files = MakeTestDataFromString("3,4,5");
    const int expectedValue = 5;
    InitializeAndVerifyExpectedValue(files, FileName, CreateRollingFileAppender("3,0,1"), expectedValue);
  }

  /// <summary>
  /// Verifies that count goes to the highest when counting up
  /// </summary>
  [Test]
  public void TestInitializeCountUpFixed2()
  {
    List<string> files = MakeTestDataFromString("0,3");
    const int expectedValue = 3;
    InitializeAndVerifyExpectedValue(files, FileName, CreateRollingFileAppender("3,0,1"), expectedValue);
  }

  /// <summary>
  /// Verifies that count stays at 0 for the zero backups case
  /// when counting up
  /// </summary>
  [Test]
  public void TestInitializeCountUpZeroBackups()
  {
    List<string> files = MakeTestDataFromString("0,3");
    const int expectedValue = 0;
    InitializeAndVerifyExpectedValue(files, FileName, CreateRollingFileAppender("0,0,1"), expectedValue);
  }

  /// <summary>
  /// Verifies that count stays at 0 for the zero backups case
  /// when counting down
  /// </summary>
  [Test]
  public void TestInitializeCountDownZeroBackups()
  {
    List<string> files = MakeTestDataFromString("0,3");
    const int expectedValue = 0;
    InitializeAndVerifyExpectedValue(files, FileName, CreateRollingFileAppender("0,0,-1"), expectedValue);
  }


  /// <summary>
  /// Verifies that count goes to the highest when counting up
  /// </summary>
  [Test]
  public void TestInitializeCountDownFixed()
  {
    List<string> files = MakeTestDataFromString("4,5,6");
    VerifyInitializeDownFixedExpectedValue(files, FileName, 0);
  }

  /// <summary>
  /// Verifies that count goes to the highest when counting up
  /// </summary>
  [Test]
  public void TestInitializeCountDownFixed2()
  {
    List<string> files = MakeTestDataFromString("1,5,6");
    VerifyInitializeDownFixedExpectedValue(files, FileName, 1);
  }

  /// <summary>
  /// Verifies that count goes to the highest when counting up
  /// </summary>
  [Test]
  public void TestInitializeCountDownFixed3()
  {
    List<string> files = MakeTestDataFromString("2,5,6");
    VerifyInitializeDownFixedExpectedValue(files, FileName, 2);
  }

  /// <summary>
  /// Verifies that count goes to the highest when counting up
  /// </summary>
  [Test]
  public void TestInitializeCountDownFixed4()
  {
    List<string> files = MakeTestDataFromString("3,5,6");
    VerifyInitializeDownFixedExpectedValue(files, FileName, 3);
  }

  /// <summary>
  /// Verifies that count goes to the highest when counting up
  /// </summary>
  [Test]
  public void TestInitializeCountDownFixed5()
  {
    List<string> files = MakeTestDataFromString("1,2,3");
    VerifyInitializeDownFixedExpectedValue(files, FileName, 3);
  }

  /// <summary>
  /// Verifies that count goes to the highest when counting up
  /// </summary>
  [Test]
  public void TestInitializeCountDownFixed6()
  {
    List<string> files = MakeTestDataFromString("1,2");
    VerifyInitializeDownFixedExpectedValue(files, FileName, 2);
  }

  /// <summary>
  /// Verifies that count goes to the highest when counting up
  /// </summary>
  [Test]
  public void TestInitializeCountDownFixed7()
  {
    List<string> files = MakeTestDataFromString("2,3");
    VerifyInitializeDownFixedExpectedValue(files, FileName, 3);
  }

  private static void InitializeAndVerifyExpectedValue(List<string> files, string baseFile,
    RollingFileAppender rfa, int expectedValue)
  {
    rfa.InitializeRollBackups(baseFile, files);
    Assert.That(rfa.CurrentSizeRollBackups, Is.EqualTo(expectedValue));
  }

  /// <summary>
  /// Tests the count-down case, with infinite max backups, to see that
  /// initialization of the rolling file appender results in the expected value
  /// </summary>
  /// <param name="files"></param>
  /// <param name="baseFile"></param>
  /// <param name="expectedValue"></param>
  private static void VerifyInitializeDownInfiniteExpectedValue(List<string> files,
    string baseFile, int expectedValue)
    => InitializeAndVerifyExpectedValue(files, baseFile, CreateRollingFileAppender("-1,0,-1"), expectedValue);

  /// <summary>
  /// Creates a RollingFileAppender with the desired values, where the
  /// values are passed as a comma separated string, with 3 parameters,
  /// maxSizeRollBackups, curSizeRollBackups, CountDirection
  /// </summary>
  /// <returns></returns>
  private static RollingFileAppender CreateRollingFileAppender(string parameters)
  {
    string[] asParams = parameters.Split(',');
    if (asParams.Length != 3)
    {
      throw new ArgumentOutOfRangeException(parameters, parameters,
        "Must have 3 comma separated params: MaxSizeRollBackups, CurSizeRollBackups, CountDirection");
    }

    RollingFileAppender rfa = new()
    {
      RollingStyle = RollingFileAppender.RollingMode.Size,
      MaxSizeRollBackups = int.Parse(asParams[0].Trim()),
      CurrentSizeRollBackups = int.Parse(asParams[1].Trim()),
      CountDirection = int.Parse(asParams[2].Trim())
    };

    return rfa;
  }

  /// <summary>
  /// Verifies that count goes to the highest when counting down
  /// and infinite backups are selected
  /// </summary>
  [Test]
  public void TestInitializeCountDownInfinite()
  {
    List<string> files = MakeTestDataFromString("2,3");
    VerifyInitializeDownInfiniteExpectedValue(files, FileName, 3);
  }

  /// <summary>
  /// Verifies that count goes to the highest when counting down
  /// and infinite backups are selected
  /// </summary>
  [Test]
  public void TestInitializeCountDownInfinite2()
  {
    List<string> files = MakeTestDataFromString("2,3,4,5,6,7,8,9,10");
    VerifyInitializeDownInfiniteExpectedValue(files, FileName, 10);
  }

  /// <summary>
  /// Verifies that count goes to the highest when counting down
  /// and infinite backups are selected
  /// </summary>
  [Test]
  public void TestInitializeCountDownInfinite3()
  {
    List<string> files = MakeTestDataFromString("9,10,3,4,5,7,9,6,1,2,8");
    VerifyInitializeDownInfiniteExpectedValue(files, FileName, 10);
  }

  /// <summary>
  /// Verifies that count goes to the highest when counting up
  /// and infinite backups are selected
  /// </summary>
  [Test]
  public void TestInitializeCountUpInfinite()
  {
    List<string> files = MakeTestDataFromString("2,3");
    VerifyInitializeUpInfiniteExpectedValue(files, FileName, 3);
  }

  /// <summary>
  /// Verifies that count goes to the highest when counting up
  /// and infinite backups are selected
  /// </summary>
  [Test]
  public void TestInitializeCountUpInfinite2()
  {
    List<string> files = MakeTestDataFromString("2,3,4,5,6,7,8,9,10");
    VerifyInitializeUpInfiniteExpectedValue(files, FileName, 10);
  }

  /// <summary>
  /// Verifies that count goes to the highest when counting up
  /// and infinite backups are selected
  /// </summary>
  [Test]
  public void TestInitializeCountUpInfinite3()
  {
    List<string> files = MakeTestDataFromString("9,10,3,4,5,7,9,6,1,2,8");
    VerifyInitializeUpInfiniteExpectedValue(files, FileName, 10);
  }

  /// <summary>
  /// Creates a logger hierarchy, configures a rolling file appender and returns an ILogger
  /// </summary>
  /// <param name="filename">The filename to log to</param>
  /// <param name="lockModel">The locking model to use.</param>
  /// <param name="handler">The error handler to use.</param>
  /// <param name="maxFileSize">Maximum file size for roll</param>
  /// <param name="maxSizeRollBackups">Maximum number of roll backups</param>
  /// <returns>A configured ILogger</returns>
  private static ILogger CreateLogger(string filename, FileAppender.LockingModelBase? lockModel,
    IErrorHandler handler, int maxFileSize = 100000, int maxSizeRollBackups = 0)
  {
    Repository.Hierarchy.Hierarchy h =
      (Repository.Hierarchy.Hierarchy)LogManager.CreateRepository("TestRepository");

    RollingFileAppender appender = new()
    {
      File = filename,
      AppendToFile = false,
      CountDirection = 0,
      RollingStyle = RollingFileAppender.RollingMode.Size,
      MaxFileSize = maxFileSize,
      Encoding = Encoding.ASCII,
      ErrorHandler = handler,
      MaxSizeRollBackups = maxSizeRollBackups
    };
    if (lockModel is not null)
    {
      appender.LockingModel = lockModel;
    }

    PatternLayout layout = new()
    {
      ConversionPattern = "%m%n"
    };
    layout.ActivateOptions();

    appender.Layout = layout;
    appender.ActivateOptions();

    h.Root.AddAppender(appender);
    h.Configured = true;

    ILogger log = h.GetLogger("Logger");
    return log;
  }

  /// <summary>
  /// Destroys the logger hierarchy created by <see cref="CreateLogger"/>
  /// </summary>
  private static void DestroyLogger()
  {
    Repository.Hierarchy.Hierarchy h =
        (Repository.Hierarchy.Hierarchy)LogManager.GetRepository("TestRepository");
    h.ResetConfiguration();
    //Replace the repository selector so that we can recreate the hierarchy with the same name if necessary
    LoggerManager.RepositorySelector =
      new DefaultRepositorySelector(typeof(Repository.Hierarchy.Hierarchy));
  }

  private static void AssertFileEquals(string filename, string contents)
  {
    string logContent = File.ReadAllText(filename);
    Assert.That(logContent, Is.EqualTo(contents), "Log contents is not what is expected");
    File.Delete(filename);
  }

  /// <summary>
  /// Verifies that logging a message actually produces output
  /// </summary>
  [Test]
  public void TestLogOutput()
  {
    const string filename = "test_simple.log";
    SilentErrorHandler sh = new();
    ILogger log = CreateLogger(filename, new FileAppender.ExclusiveLock(), sh);
    log.Log(GetType(), Level.Info, "This is a message", null);
    log.Log(GetType(), Level.Info, "This is a message 2", null);
    DestroyLogger();

    AssertFileEquals(filename,
      "This is a message" + Environment.NewLine + "This is a message 2" + Environment.NewLine);
    Assert.That(sh.Message, Is.EqualTo(""), "Unexpected error message");
  }

  /// <summary>
  /// Verifies that attempting to log to a locked file fails gracefully
  /// </summary>
  [Test]
  public void TestExclusiveLockFails()
  {
    const string filename = "test_exclusive_lock_fails.log";

    FileStream fs = new(filename, FileMode.Create, FileAccess.Write, FileShare.None);
    fs.Write("Test"u8.ToArray(), 0, 4);

    SilentErrorHandler sh = new();
    ILogger log = CreateLogger(filename, new FileAppender.ExclusiveLock(), sh);
    log.Log(GetType(), Level.Info, "This is a message", null);
    log.Log(GetType(), Level.Info, "This is a message 2", null);
    DestroyLogger();
    fs.Close();

    AssertFileEquals(filename, "Test");
    Assert.That(sh.Message, Does.StartWith("Unable to acquire lock on file"), "Expecting an error message");
  }

  /// <summary>
  /// Verifies that attempting to log to a locked file recovers if the lock is released
  /// </summary>
  [Test]
  public void TestExclusiveLockRecovers()
  {
    const string filename = "test_exclusive_lock_recovers.log";

    FileStream fs = new(filename, FileMode.Create, FileAccess.Write, FileShare.None);
    fs.Write("Test"u8.ToArray(), 0, 4);

    SilentErrorHandler sh = new();
    ILogger log = CreateLogger(filename, new FileAppender.ExclusiveLock(), sh);
    log.Log(GetType(), Level.Info, "This is a message", null);
    fs.Close();
    log.Log(GetType(), Level.Info, "This is a message 2", null);
    DestroyLogger();

    AssertFileEquals(filename, "This is a message 2" + Environment.NewLine);
    Assert.That(sh.Message, Does.StartWith("Unable to acquire lock on file"),
      "Expecting an error message");
  }

  /// <summary>
  /// Verifies that attempting to log to a file with ExclusiveLock really locks the file
  /// </summary>
  [Test]
  public void TestExclusiveLockLocks()
  {
    const string filename = "test_exclusive_lock_locks.log";
    bool locked = false;

    SilentErrorHandler sh = new();
    ILogger log = CreateLogger(filename, new FileAppender.ExclusiveLock(), sh);
    log.Log(GetType(), Level.Info, "This is a message", null);

    try
    {
      FileStream fs = new(filename, FileMode.Create, FileAccess.Write, FileShare.None);
      fs.Write("Test"u8.ToArray(), 0, 4);
      fs.Close();
    }
    catch (IOException e1)
    {
      Assert.That(e1.Message.Substring(0, 35), Is.EqualTo("The process cannot access the file "),
      "Unexpected exception");
      locked = true;
    }

    log.Log(GetType(), Level.Info, "This is a message 2", null);
    DestroyLogger();

    Assert.That(locked, "File was not locked");
    AssertFileEquals(filename,
      "This is a message" + Environment.NewLine + "This is a message 2" + Environment.NewLine);
    Assert.That(sh.Message, Is.EqualTo(""), "Unexpected error message");
  }


  /// <summary>
  /// Verifies that attempting to log to a locked file fails gracefully
  /// </summary>
  [Test]
  public void TestMinimalLockFails()
  {
    const string filename = "test_minimal_lock_fails.log";

    FileStream fs = new(filename, FileMode.Create, FileAccess.Write, FileShare.None);
    fs.Write("Test"u8.ToArray(), 0, 4);

    SilentErrorHandler sh = new();
    ILogger log = CreateLogger(filename, new FileAppender.MinimalLock(), sh);
    log.Log(GetType(), Level.Info, "This is a message", null);
    log.Log(GetType(), Level.Info, "This is a message 2", null);
    DestroyLogger();
    fs.Close();

    AssertFileEquals(filename, "Test");
    Assert.That(sh.Message.Substring(0, 30), Is.EqualTo("Unable to acquire lock on file"),
        "Expecting an error message");
  }

  /// <summary>
  /// Verifies that attempting to log to a locked file recovers if the lock is released
  /// </summary>
  [Test]
  public void TestMinimalLockRecovers()
  {
    const string filename = "test_minimal_lock_recovers.log";

    FileStream fs = new(filename, FileMode.Create, FileAccess.Write, FileShare.None);
    fs.Write("Test"u8.ToArray(), 0, 4);

    SilentErrorHandler sh = new();
    ILogger log = CreateLogger(filename, new FileAppender.MinimalLock(), sh);
    log.Log(GetType(), Level.Info, "This is a message", null);
    fs.Close();
    log.Log(GetType(), Level.Info, "This is a message 2", null);
    DestroyLogger();

    AssertFileEquals(filename, "This is a message 2" + Environment.NewLine);
    Assert.That(sh.Message.Substring(0, 30), Is.EqualTo("Unable to acquire lock on file"),
      "Expecting an error message");
  }

  /// <summary>
  /// Verifies that attempting to log to a file with MinimalLock doesn't lock the file
  /// </summary>
  [Test]
  public void TestMinimalLockUnlocks()
  {
    const string filename = "test_minimal_lock_unlocks.log";

    SilentErrorHandler sh = new();
    ILogger log = CreateLogger(filename, new FileAppender.MinimalLock(), sh);
    log.Log(GetType(), Level.Info, "This is a message", null);

    FileStream fs = new(filename, FileMode.Append, FileAccess.Write, FileShare.None);
    fs.Write(Encoding.ASCII.GetBytes("Test" + Environment.NewLine), 0, 4 + Environment.NewLine.Length);
    fs.Close();

    log.Log(GetType(), Level.Info, "This is a message 2", null);
    DestroyLogger();

    AssertFileEquals(filename,
      "This is a message" + Environment.NewLine + "Test" + Environment.NewLine + "This is a message 2" +
      Environment.NewLine);
    Assert.That(sh.Message, Is.EqualTo(""), "Unexpected error message");
  }

  /// <summary>
  /// Verifies that attempting to log to a locked file fails gracefully
  /// </summary>
  [Test]
  public void TestInterProcessLockFails()
  {
    const string filename = "test_interprocess_lock_fails.log";

    FileStream fs = new(filename, FileMode.Create, FileAccess.Write, FileShare.None);
    fs.Write("Test"u8.ToArray(), 0, 4);

    SilentErrorHandler sh = new();
    ILogger log = CreateLogger(filename, new FileAppender.InterProcessLock(), sh);
    log.Log(GetType(), Level.Info, "This is a message", null);
    log.Log(GetType(), Level.Info, "This is a message 2", null);
    DestroyLogger();
    fs.Close();

    AssertFileEquals(filename, "Test");
    Assert.That(sh.Message.Substring(0, 30), Is.EqualTo("Unable to acquire lock on file"),
        "Expecting an error message");
  }

  /// <summary>
  /// Verifies that attempting to log to a locked file recovers if the lock is released
  /// </summary>
  [Test]
  public void TestInterProcessLockRecovers()
  {
    const string filename = "test_interprocess_lock_recovers.log";

    FileStream fs = new(filename, FileMode.Create, FileAccess.Write, FileShare.None);
    fs.Write("Test"u8.ToArray(), 0, 4);

    SilentErrorHandler sh = new();
    ILogger log = CreateLogger(filename, new FileAppender.InterProcessLock(), sh);
    log.Log(GetType(), Level.Info, "This is a message", null);
    fs.Close();
    log.Log(GetType(), Level.Info, "This is a message 2", null);
    DestroyLogger();

    AssertFileEquals(filename, "This is a message 2" + Environment.NewLine);
    Assert.That(sh.Message.Substring(0, 30), Is.EqualTo("Unable to acquire lock on file"),
      "Expecting an error message");
  }

  /// <summary>
  /// Verifies that attempting to log to a file with InterProcessLock really locks the file
  /// </summary>
  [Test]
  public void TestInterProcessLockUnlocks()
  {
    const string filename = "test_interprocess_lock_unlocks.log";

    SilentErrorHandler sh = new();
    ILogger log = CreateLogger(filename, new FileAppender.InterProcessLock(), sh);
    log.Log(GetType(), Level.Info, "This is a message", null);

    FileStream fs = new(filename, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
    fs.Write(Encoding.ASCII.GetBytes("Test" + Environment.NewLine), 0, 4 + Environment.NewLine.Length);
    fs.Close();

    log.Log(GetType(), Level.Info, "This is a message 2", null);
    DestroyLogger();

    AssertFileEquals(filename,
      "This is a message" + Environment.NewLine + "Test" + Environment.NewLine + "This is a message 2" +
      Environment.NewLine);
    Assert.That(sh.Message, Is.EqualTo(""), "Unexpected error message");
  }

  /// <summary>
  /// Verifies that rolling file works
  /// </summary>
  [Test]
  public void TestInterProcessLockRoll()
  {
    const string filename = "test_interprocess_lock_roll.log";

    SilentErrorHandler sh = new();
    ILogger log = CreateLogger(filename, new FileAppender.InterProcessLock(), sh, 1, 2);

    Assert.DoesNotThrow(() => log.Log(GetType(), Level.Info, "A", null));
    Assert.DoesNotThrow(() => log.Log(GetType(), Level.Info, "A", null));

    DestroyLogger();

    AssertFileEquals(filename, "A" + Environment.NewLine);
    AssertFileEquals(filename + ".1", "A" + Environment.NewLine);
    Assert.That(sh.Message, Is.Empty);
  }

  /// <summary>
  /// Verify that the default LockModel is ExclusiveLock, to maintain backwards compatibility with previous behaviour
  /// </summary>
  [Test]
  public void TestDefaultLockingModel()
  {
    const string filename = "test_default.log";

    SilentErrorHandler sh = new();
    ILogger log = CreateLogger(filename, null, sh);

    IAppender[]? appenders = log.Repository?.GetAppenders();
    Assert.That(appenders, Is.Not.Null);
    Assert.That(appenders!, Has.Length.EqualTo(1), "The wrong number of appenders are configured");

    RollingFileAppender rfa = (RollingFileAppender)(appenders[0]);
    Assert.That(rfa.LockingModel.GetType(), Is.EqualTo(typeof(FileAppender.ExclusiveLock)),
        "The LockingModel is of an unexpected type");

    DestroyLogger();
  }

  /// <summary>
  /// Tests the count up case, with infinite max backups , to see that
  /// initialization of the rolling file appender results in the expected value
  /// </summary>
  private static void VerifyInitializeUpInfiniteExpectedValue(List<string> files,
    string baseFile, int expectedValue)
    => InitializeAndVerifyExpectedValue(files, baseFile, CreateRollingFileAppender("-1,0,1"), expectedValue);


  /// <summary>
  /// Tests the countdown case, with max backups limited to 3, to see that
  /// initialization of the rolling file appender results in the expected value
  /// </summary>
  private static void VerifyInitializeDownFixedExpectedValue(List<string> files, string baseFile,
    int expectedValue)
    => InitializeAndVerifyExpectedValue(files, baseFile, CreateRollingFileAppender("3,0,-1"), expectedValue);

  /// <summary>
  /// Turns a string of comma separated numbers into a collection of filenames
  /// generated from the numbers.  
  /// 
  /// Defaults to filename in _fileName variable.
  /// 
  /// </summary>
  /// <param name="fileNumbers">Comma separated list of numbers for counted file names</param>
  private List<string> MakeTestDataFromString(string fileNumbers)
    => MakeTestDataFromString(FileName, fileNumbers);

  /// <summary>
  /// Turns a string of comma separated numbers into a collection of filenames
  /// generated from the numbers
  /// 
  /// Uses the input filename.
  /// </summary>
  /// <param name="sFileName">Name of file to combine with numbers when generating counted file names</param>
  /// <param name="fileNumbers">Comma separated list of numbers for counted file names</param>
  /// <returns></returns>
  private static List<string> MakeTestDataFromString(string sFileName, string fileNumbers) 
    => fileNumbers.Split(',').Select(number => int.Parse(number.Trim()))
      .Select(value => MakeFileName(sFileName, value)).ToList();

  /// <summary>
  /// Tests that the current backup index is correctly detected
  /// for a file with no extension
  /// </summary>
  [Test]
  public void TestInitializeRollBackups2() => VerifyInitializeRollBackupsFromBaseFile("LogFile");

  /// <summary>
  /// Tests that the current backup index is correctly detected
  /// for a file with a .log extension
  /// </summary>
  [Test]
  public void TestInitializeRollBackups3() => VerifyInitializeRollBackupsFromBaseFile("LogFile.log");

  /// <summary>
  /// Makes sure that the initialization can detect the backup
  /// number correctly.
  /// </summary>
  private static void VerifyInitializeRollBackups(int backups, int maxSizeRollBackups)
  {
    const string baseFile = "LogFile.log";
    List<string> arrFiles = ["junk1"];
    for (int i = 0; i < backups; i++)
    {
      arrFiles.Add(MakeFileName(baseFile, i));
    }

    RollingFileAppender rfa = new()
    {
      RollingStyle = RollingFileAppender.RollingMode.Size,
      MaxSizeRollBackups = maxSizeRollBackups,
      CurrentSizeRollBackups = 0
    };
    rfa.InitializeRollBackups(baseFile, arrFiles);

    // iBackups  / Meaning
    // 0 = none
    // 1 = file.log
    // 2 = file.log.1
    // 3 = file.log.2
    Assert.That(rfa.CurrentSizeRollBackups, backups is 0 or 1 
      ? Is.EqualTo(0) 
      : Is.EqualTo(Math.Min(backups - 1, maxSizeRollBackups)));
  }

  /// <summary>
  /// Tests that the current backup index is correctly detected,
  /// and gets no bigger than the max backups setting
  /// </summary>
  [Test]
  public void TestInitializeRollBackups4()
  {
    const int maxRollBackups = 5;
    VerifyInitializeRollBackups(0, maxRollBackups);
    VerifyInitializeRollBackups(1, maxRollBackups);
    VerifyInitializeRollBackups(2, maxRollBackups);
    VerifyInitializeRollBackups(3, maxRollBackups);
    VerifyInitializeRollBackups(4, maxRollBackups);
    VerifyInitializeRollBackups(5, maxRollBackups);
    VerifyInitializeRollBackups(6, maxRollBackups);
    // Final we cap out at the max value
    VerifyInitializeRollBackups(7, maxRollBackups);
    VerifyInitializeRollBackups(8, maxRollBackups);
  }

  /// <summary>
  /// Ensures that no problems result from creating and then closing the appender
  /// when it has not also been initialized with ActivateOptions().
  /// </summary>
  [Test]
  public void TestCreateCloseNoActivateOptions() => new RollingFileAppender().Close();

  //
  // Helper functions to dig into the appender
  //

  private static List<string> GetExistingFiles(string baseFilePath, bool preserveLogFileNameExtension = false)
  {
    RollingFileAppenderForTest appender = new()
    {
      PreserveLogFileNameExtension = preserveLogFileNameExtension,
      SecurityContext = NullSecurityContext.Instance
    };
    return appender.GetExistingFiles(baseFilePath);
  }

  private static string GetTestMessage() => Environment.NewLine.Length switch
  {
    2 => TestMessage98Chars,
    1 => TestMessage99Chars,
    _ => throw new InvalidOperationException("Unexpected Environment.NewLine.Length"),
  };
}

[TestFixture]
public sealed class RollingFileAppenderSubClassTest : RollingFileAppender
{
  [Test]
  public void TestComputeCheckPeriod()
  {
    Assert.That(ComputeCheckPeriod(".yyyy-MM-dd HH:mm"), Is.EqualTo(RollPoint.TopOfMinute), "TopOfMinute pattern");
    Assert.That(ComputeCheckPeriod(".yyyy-MM-dd HH"), Is.EqualTo(RollPoint.TopOfHour), "TopOfHour pattern");
    Assert.That(ComputeCheckPeriod(".yyyy-MM-dd tt"), Is.EqualTo(RollPoint.HalfDay), "HalfDay pattern");
    Assert.That(ComputeCheckPeriod(".yyyy-MM-dd"), Is.EqualTo(RollPoint.TopOfDay), "TopOfDay pattern");
    Assert.That(ComputeCheckPeriod(".yyyy-MM"), Is.EqualTo(RollPoint.TopOfMonth), "TopOfMonth pattern");

    // Test invalid roll point
    Assert.That(ComputeCheckPeriod("..."), Is.EqualTo(RollPoint.InvalidRollPoint), "TopOfMonth pattern");
  }
}