using System;
using System.Collections;
using System.IO;
using System.Threading.Tasks;

using AIRuntime.Runtime.Filesystem;

using NUnit.Framework;

using UnityEngine.TestTools;


namespace AIRuntime.Tests
{
    public sealed class LocalFileSystemTests
    {
        private string _workspacePath;

        private IFileSystem _fileSystem;


        [SetUp]
        public void SetUp()
        {
            _workspacePath =
                Path.Combine(
                    Path.GetTempPath(),
                    "AIRuntime.LocalFileSystemTests",
                    Guid.NewGuid().ToString("N"));


            Directory.CreateDirectory(
                _workspacePath);


            _fileSystem =
                new LocalFileSystem(
                    new Workspace(
                        _workspacePath));
        }


        [TearDown]
        public void TearDown()
        {
            if (
                !string.IsNullOrWhiteSpace(
                    _workspacePath) &&
                Directory.Exists(
                    _workspacePath))
            {
                Directory.Delete(
                    _workspacePath,
                    recursive: true);
            }
        }


        [UnityTest]
        public IEnumerator EndToEndFileOperations_WorkInsideIsolatedWorkspace()
        {
            Task task =
                RunEndToEndFileOperationsAsync();


            while (!task.IsCompleted)
            {
                yield return null;
            }


            if (task.IsFaulted)
            {
                throw task.Exception?
                    .GetBaseException();
            }


            if (task.IsCanceled)
            {
                throw new OperationCanceledException(
                    "The filesystem test was unexpectedly cancelled.");
            }
        }


        [Test]
        public void MoveAndCopyRejectIdenticalPaths()
        {
            File.WriteAllText(
                Path.Combine(
                    _workspacePath,
                    "same.txt"),
                "preserve me");


            Assert.Throws<ArgumentException>(
                () => _fileSystem.MoveFileAsync(
                    "same.txt",
                    "same.txt"));

            Assert.Throws<ArgumentException>(
                () => _fileSystem.CopyFileAsync(
                    "same.txt",
                    "same.txt"));


            Assert.AreEqual(
                "preserve me",
                File.ReadAllText(
                    Path.Combine(
                        _workspacePath,
                        "same.txt")));
        }


        [Test]
        public void MoveReplacesExistingDestination()
        {
            string sourcePath =
                Path.Combine(
                    _workspacePath,
                    "source.txt");

            string destinationPath =
                Path.Combine(
                    _workspacePath,
                    "destination.txt");

            File.WriteAllText(
                sourcePath,
                "new contents");

            File.WriteAllText(
                destinationPath,
                "original contents");


            _fileSystem.MoveFileAsync(
                "source.txt",
                "destination.txt");


            Assert.IsFalse(
                File.Exists(sourcePath));

            Assert.AreEqual(
                "new contents",
                File.ReadAllText(
                    destinationPath));
        }


        [Test]
        public void FailedMovePreservesExistingDestination()
        {
            if (Path.DirectorySeparatorChar != '\\')
            {
                Assert.Ignore(
                    "This regression test requires Windows file sharing semantics.");
            }


            string sourcePath =
                Path.Combine(
                    _workspacePath,
                    "locked-source.txt");

            string destinationPath =
                Path.Combine(
                    _workspacePath,
                    "destination.txt");

            File.WriteAllText(
                sourcePath,
                "new contents");

            File.WriteAllText(
                destinationPath,
                "original contents");


            using (new FileStream(
                       sourcePath,
                       FileMode.Open,
                       FileAccess.Read,
                       FileShare.Read))
            {
                Assert.Catch<IOException>(
                    () => _fileSystem.MoveFileAsync(
                        "locked-source.txt",
                        "destination.txt"));


                Assert.AreEqual(
                    "original contents",
                    File.ReadAllText(
                        destinationPath));

                Assert.IsTrue(
                    File.Exists(sourcePath));
            }
        }


        [Test]
        public void DeleteDirectoryRejectsWorkspaceRoot()
        {
            Assert.Throws<InvalidOperationException>(
                () => _fileSystem.DeleteDirectoryAsync(
                    ".",
                    recursive: true));


            Assert.IsTrue(
                Directory.Exists(
                    _workspacePath));
        }


        [UnityTest]
        public IEnumerator AtomicWrite_RejectsStaleHashAndPreservesCurrentFile()
        {
            IAtomicFileSystem atomic = (IAtomicFileSystem)_fileSystem;
            Task<AtomicFileWriteResult> create = atomic.WriteFileAtomicallyAsync(
                "guarded.txt", "first", null, true, false);
            yield return WaitForCompletion(create);
            create.GetAwaiter().GetResult();

            Task<AtomicFileWriteResult> replace = atomic.WriteFileAtomicallyAsync(
                "guarded.txt", "stale replacement", new string('0', 64), false, true);
            yield return WaitForCompletion(replace);
            Assert.Throws<IOException>(() => replace.GetAwaiter().GetResult());
            Assert.AreEqual("first", File.ReadAllText(Path.Combine(_workspacePath, "guarded.txt")));
        }


        [UnityTest]
        public IEnumerator AtomicWrite_ReplacesExactObservedVersionAndReturnsHashes()
        {
            IAtomicFileSystem atomic = (IAtomicFileSystem)_fileSystem;
            Task<AtomicFileWriteResult> create = atomic.WriteFileAtomicallyAsync(
                "guarded.txt", "first", null, true, false);
            yield return WaitForCompletion(create);
            AtomicFileWriteResult created = create.GetAwaiter().GetResult();

            Task<AtomicFileWriteResult> replace = atomic.WriteFileAtomicallyAsync(
                "guarded.txt", "second", created.CurrentSha256, false, false);
            yield return WaitForCompletion(replace);
            AtomicFileWriteResult replaced = replace.GetAwaiter().GetResult();

            Assert.IsTrue(replaced.Replaced);
            Assert.AreEqual(created.CurrentSha256, replaced.PreviousSha256);
            Assert.AreNotEqual(replaced.PreviousSha256, replaced.CurrentSha256);
            Assert.AreEqual("second", File.ReadAllText(Path.Combine(_workspacePath, "guarded.txt")));
        }


        private static IEnumerator WaitForCompletion(Task task)
        {
            // Keep Unity's main thread free to run captured async continuations.
            var stopwatch = System.Diagnostics.Stopwatch.StartNew();
            while (!task.IsCompleted)
            {
                if (stopwatch.Elapsed.TotalSeconds >= 10)
                {
                    Assert.Fail("The atomic filesystem operation did not complete within 10 seconds.");
                }

                yield return null;
            }
        }


        private async Task RunEndToEndFileOperationsAsync()
        {
            await _fileSystem.CreateDirectoryAsync(
                "workspace");


            Assert.IsTrue(
                await _fileSystem.DirectoryExistsAsync(
                    "workspace"));


            await _fileSystem.WriteFileAsync(
                "workspace/source.txt",
                "Hello AIRuntime!");


            Assert.IsTrue(
                await _fileSystem.FileExistsAsync(
                    "workspace/source.txt"));


            await _fileSystem.AppendFileAsync(
                "workspace/source.txt",
                " Appended.");


            Assert.AreEqual(
                "Hello AIRuntime!\n Appended.",
                await _fileSystem.ReadFileAsync(
                    "workspace/source.txt"));


            await _fileSystem.CopyFileAsync(
                "workspace/source.txt",
                "workspace/copy.txt");


            await _fileSystem.MoveFileAsync(
                "workspace/copy.txt",
                "workspace/moved.txt");


            Assert.IsFalse(
                await _fileSystem.FileExistsAsync(
                    "workspace/copy.txt"));


            Assert.IsTrue(
                await _fileSystem.FileExistsAsync(
                    "workspace/moved.txt"));


            var entries =
                await _fileSystem.ListDirectoryAsync(
                    "workspace");


            CollectionAssert.Contains(
                entries,
                "source.txt");


            CollectionAssert.Contains(
                entries,
                "moved.txt");


            var info =
                await _fileSystem.GetFileInfoAsync(
                    "workspace/source.txt");


            Assert.IsTrue(
                info.Exists);


            Assert.IsFalse(
                info.IsDirectory);


            Assert.AreEqual(
                "source.txt",
                info.Name);


            Assert.Greater(
                info.Size,
                0);


            await _fileSystem.DeleteFileAsync(
                "workspace/moved.txt");


            Assert.IsFalse(
                await _fileSystem.FileExistsAsync(
                    "workspace/moved.txt"));


            await _fileSystem.DeleteDirectoryAsync(
                "workspace",
                recursive: true);


            Assert.IsFalse(
                await _fileSystem.DirectoryExistsAsync(
                    "workspace"));
        }
    }
}
