using CodeRemoteManager.SetupApp;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Nodes;

namespace CodeRemoteManager.SetupApp.Tests;

[TestClass]
public sealed class ConfigurationStoreTests
{
    [TestMethod]
    public async Task UpgradeCreatesBackupBeforeReplacingConfiguration()
    {
        var directory = Directory.CreateTempSubdirectory("crm-config-store-");
        try
        {
            var configPath = Path.Combine(directory.FullName, "config.local.json");
            const string existing = """
            {
              "codexCliPath": "old-codex.cmd",
              "engine": "codex",
              "bindHost": "127.0.0.1",
              "port": 43110,
              "dataDir": "private-data",
              "accounts": [{ "id": "existing", "label": "Existing", "codexHome": "home" }],
              "workspaces": [{ "id": "workspace", "label": "Workspace", "path": "project" }]
            }
            """;
            await File.WriteAllTextAsync(configPath, existing);
            var candidate = ConfigurationMigration.BuildCandidate(
                existing,
                new SetupConfigurationInput("current-codex.cmd", "https://device.example.ts.net", null),
                new SetupConfigurationDefaults("default-data", "desktop-home"));
            var store = new ConfigurationStore(ValidateJsonAsync);

            await store.SaveAsync(configPath, candidate);

            var backupDirectory = Path.Combine(directory.FullName, "config-backups");
            var backups = Directory.GetFiles(backupDirectory, "*.json");
            Assert.AreEqual(1, backups.Length);
            var backup = JsonNode.Parse(await File.ReadAllTextAsync(backups[0]))!.AsObject();
            Assert.AreEqual("existing", backup["accounts"]![0]!["id"]!.GetValue<string>());
            var saved = JsonNode.Parse(await File.ReadAllTextAsync(configPath))!.AsObject();
            Assert.AreEqual(1, saved["schemaVersion"]!.GetValue<int>());
            Assert.AreEqual("current-codex.cmd", saved["codexCliPath"]!.GetValue<string>());
        }
        finally
        {
            directory.Delete(recursive: true);
        }
    }

    [TestMethod]
    public async Task AtomicReplaceFailureLeavesOriginalConfigurationUntouched()
    {
        var directory = Directory.CreateTempSubdirectory("crm-config-store-");
        try
        {
            var configPath = Path.Combine(directory.FullName, "config.local.json");
            var existing = ExistingConfiguration("original");
            await File.WriteAllTextAsync(configPath, existing);
            var candidate = Candidate(existing);
            var store = new ConfigurationStore(
                ValidateJsonAsync,
                (_, _, _) => throw new IOException("simulated replace failure"));

            var error = await Assert.ThrowsExceptionAsync<ConfigurationMigrationException>(() =>
                store.SaveAsync(configPath, candidate));

            Assert.AreEqual("configuration_write_failed", error.Message);
            var saved = JsonNode.Parse(await File.ReadAllTextAsync(configPath))!.AsObject();
            Assert.AreEqual("original", saved["accounts"]![0]!["id"]!.GetValue<string>());
            Assert.IsFalse(File.Exists($"{configPath}.pending"));
        }
        finally
        {
            directory.Delete(recursive: true);
        }
    }

    [TestMethod]
    public async Task FinalValidationFailureRestoresOriginalBackup()
    {
        var directory = Directory.CreateTempSubdirectory("crm-config-store-");
        try
        {
            var configPath = Path.Combine(directory.FullName, "config.local.json");
            var existing = ExistingConfiguration("original");
            await File.WriteAllTextAsync(configPath, existing);
            var candidate = Candidate(existing);
            var failFinalValidationOnce = true;
            var store = new ConfigurationStore(async (path, cancellationToken) =>
            {
                await ValidateJsonAsync(path, cancellationToken);
                if (path == configPath && failFinalValidationOnce)
                {
                    failFinalValidationOnce = false;
                    throw new InvalidOperationException("simulated final validation failure");
                }
            });

            var error = await Assert.ThrowsExceptionAsync<ConfigurationMigrationException>(() =>
                store.SaveAsync(configPath, candidate));

            Assert.AreEqual("configuration_write_failed", error.Message);
            var restored = JsonNode.Parse(await File.ReadAllTextAsync(configPath))!.AsObject();
            Assert.AreEqual("original", restored["accounts"]![0]!["id"]!.GetValue<string>());
            Assert.AreEqual("old-codex.cmd", restored["codexCliPath"]!.GetValue<string>());
        }
        finally
        {
            directory.Delete(recursive: true);
        }
    }

    [TestMethod]
    public async Task FirstRunFinalValidationFailureRemovesNewConfiguration()
    {
        var directory = Directory.CreateTempSubdirectory("crm-config-store-");
        try
        {
            var configPath = Path.Combine(directory.FullName, "config.local.json");
            var candidate = ConfigurationMigration.BuildCandidate(
                null,
                new SetupConfigurationInput(
                    "current-codex.cmd",
                    "https://device.example.ts.net",
                    Path.Combine(directory.FullName, "project")),
                new SetupConfigurationDefaults("default-data", "desktop-home"));
            var store = new ConfigurationStore(async (path, cancellationToken) =>
            {
                await ValidateJsonAsync(path, cancellationToken);
                if (path == configPath)
                    throw new InvalidOperationException("simulated final validation failure");
            });

            var error = await Assert.ThrowsExceptionAsync<ConfigurationMigrationException>(() =>
                store.SaveAsync(configPath, candidate));

            Assert.AreEqual("configuration_write_failed", error.Message);
            Assert.IsFalse(File.Exists(configPath));
            Assert.IsFalse(File.Exists($"{configPath}.pending"));
            Assert.IsFalse(File.Exists($"{configPath}.transaction"));
        }
        finally
        {
            directory.Delete(recursive: true);
        }
    }

    [TestMethod]
    public async Task SuccessfulUpgradesKeepOnlyThreeNewestBackups()
    {
        var directory = Directory.CreateTempSubdirectory("crm-config-store-");
        try
        {
            var configPath = Path.Combine(directory.FullName, "config.local.json");
            await File.WriteAllTextAsync(configPath, ExistingConfiguration("original"));
            var store = new ConfigurationStore(ValidateJsonAsync);

            for (var index = 1; index <= 4; index++)
            {
                var existing = await File.ReadAllTextAsync(configPath);
                var candidate = ConfigurationMigration.BuildCandidate(
                    existing,
                    new SetupConfigurationInput($"codex-{index}.cmd", "https://device.example.ts.net", null),
                    new SetupConfigurationDefaults("default-data", "desktop-home"));
                await store.SaveAsync(configPath, candidate);
            }

            var backupDirectory = Path.Combine(directory.FullName, "config-backups");
            var backups = Directory.GetFiles(backupDirectory, "config.local.*.json");
            Assert.AreEqual(3, backups.Length);
            var saved = JsonNode.Parse(await File.ReadAllTextAsync(configPath))!.AsObject();
            Assert.AreEqual("codex-4.cmd", saved["codexCliPath"]!.GetValue<string>());
        }
        finally
        {
            directory.Delete(recursive: true);
        }
    }

    [TestMethod]
    public async Task RecoveryCompletesValidatedFirstRunPendingWithoutJournal()
    {
        var directory = Directory.CreateTempSubdirectory("crm-config-store-");
        try
        {
            var configPath = Path.Combine(directory.FullName, "config.local.json");
            var pendingPath = $"{configPath}.pending";
            var transactionPath = $"{configPath}.transaction";
            var candidate = ConfigurationMigration.BuildCandidate(
                null,
                new SetupConfigurationInput(
                    "current-codex.cmd",
                    "https://device.example.ts.net",
                    Path.Combine(directory.FullName, "project")),
                new SetupConfigurationDefaults("private-data", "desktop-home"));
            await File.WriteAllTextAsync(pendingPath, candidate.ToJsonString());
            var store = new ConfigurationStore(ValidateJsonAsync);

            await store.RecoverAsync(configPath);

            Assert.IsTrue(File.Exists(configPath));
            Assert.IsFalse(File.Exists(pendingPath));
            Assert.IsFalse(File.Exists(transactionPath));
            var recovered = JsonNode.Parse(await File.ReadAllTextAsync(configPath))!.AsObject();
            Assert.AreEqual(1, recovered["schemaVersion"]!.GetValue<int>());
        }
        finally
        {
            directory.Delete(recursive: true);
        }
    }

    [TestMethod]
    public async Task RecoveryPromotesValidatedPendingFirstRunConfiguration()
    {
        var directory = Directory.CreateTempSubdirectory("crm-config-store-");
        try
        {
            var configPath = Path.Combine(directory.FullName, "config.local.json");
            var pendingPath = $"{configPath}.pending";
            var transactionPath = $"{configPath}.transaction";
            var candidate = ConfigurationMigration.BuildCandidate(
                null,
                new SetupConfigurationInput(
                    "current-codex.cmd",
                    "https://device.example.ts.net",
                    "C:\\Projects\\First Project"),
                new SetupConfigurationDefaults("private-data", "desktop-home"));
            var candidateJson = candidate.ToJsonString();
            await File.WriteAllTextAsync(pendingPath, candidateJson);
            await File.WriteAllTextAsync(transactionPath, $$"""
            {
              "version": 1,
              "mode": "create",
              "candidateHash": "{{Sha256(candidateJson)}}"
            }
            """);
            var store = new ConfigurationStore(ValidateJsonAsync);

            await store.RecoverAsync(configPath);

            Assert.IsTrue(File.Exists(configPath));
            Assert.IsFalse(File.Exists(pendingPath));
            Assert.IsFalse(File.Exists(transactionPath));
            var recovered = JsonNode.Parse(await File.ReadAllTextAsync(configPath))!.AsObject();
            Assert.AreEqual(1, recovered["schemaVersion"]!.GetValue<int>());
            Assert.AreEqual("desktop-codex", recovered["accounts"]![0]!["id"]!.GetValue<string>());
        }
        finally
        {
            directory.Delete(recursive: true);
        }
    }

    [TestMethod]
    public async Task RecoveryQuarantinesInvalidActiveFirstRunConfiguration()
    {
        var directory = Directory.CreateTempSubdirectory("crm-config-store-");
        try
        {
            var configPath = Path.Combine(directory.FullName, "config.local.json");
            var transactionPath = $"{configPath}.transaction";
            var candidate = ConfigurationMigration.BuildCandidate(
                null,
                new SetupConfigurationInput(
                    "current-codex.cmd",
                    "https://device.example.ts.net",
                    Path.Combine(directory.FullName, "project")),
                new SetupConfigurationDefaults("private-data", "desktop-home"));
            var candidateJson = candidate.ToJsonString();
            await File.WriteAllTextAsync(configPath, candidateJson);
            await File.WriteAllTextAsync(transactionPath, $$"""
            {
              "version": 1,
              "mode": "create",
              "candidateHash": "{{Sha256(candidateJson)}}"
            }
            """);
            var store = new ConfigurationStore((path, _) =>
                path == configPath
                    ? Task.FromException(new InvalidOperationException("simulated invalid candidate"))
                    : Task.CompletedTask);

            var error = await Assert.ThrowsExceptionAsync<ConfigurationMigrationException>(() =>
                store.RecoverAsync(configPath));

            Assert.AreEqual("configuration_recovery_failed", error.Message);
            Assert.IsFalse(File.Exists(configPath));
            Assert.IsFalse(File.Exists(transactionPath));
            Assert.AreEqual(
                1,
                Directory.GetFiles(Path.Combine(directory.FullName, "config-backups"), "failed-config.*.json").Length);
        }
        finally
        {
            directory.Delete(recursive: true);
        }
    }

    [TestMethod]
    public async Task RecoveryRestoresUpgradeBackupWhenActiveConfigurationIsMissing()
    {
        var directory = Directory.CreateTempSubdirectory("crm-config-store-");
        try
        {
            var configPath = Path.Combine(directory.FullName, "config.local.json");
            var pendingPath = $"{configPath}.pending";
            var transactionPath = $"{configPath}.transaction";
            var backupDirectory = Directory.CreateDirectory(Path.Combine(directory.FullName, "config-backups"));
            var backupPath = Path.Combine(backupDirectory.FullName, "config.local.backup.json");
            var original = ExistingConfiguration("original");
            var candidateJson = Candidate(original).ToJsonString();
            await File.WriteAllTextAsync(backupPath, original);
            await File.WriteAllTextAsync(pendingPath, candidateJson);
            await File.WriteAllTextAsync(transactionPath, $$"""
            {
              "version": 1,
              "mode": "replace",
              "candidateHash": "{{Sha256(candidateJson)}}",
              "originalHash": "{{Sha256(original)}}",
              "backupFile": "config.local.backup.json"
            }
            """);
            var store = new ConfigurationStore(ValidateJsonAsync);

            await store.RecoverAsync(configPath);

            var recovered = JsonNode.Parse(await File.ReadAllTextAsync(configPath))!.AsObject();
            Assert.AreEqual("old-codex.cmd", recovered["codexCliPath"]!.GetValue<string>());
            Assert.AreEqual("original", recovered["accounts"]![0]!["id"]!.GetValue<string>());
            Assert.IsFalse(File.Exists(pendingPath));
            Assert.IsFalse(File.Exists(transactionPath));
        }
        finally
        {
            directory.Delete(recursive: true);
        }
    }

    private static string ExistingConfiguration(string accountId) => $$"""
    {
      "codexCliPath": "old-codex.cmd",
      "engine": "codex",
      "bindHost": "127.0.0.1",
      "port": 43110,
      "dataDir": "private-data",
      "accounts": [{ "id": "{{accountId}}", "label": "Existing", "codexHome": "home" }],
      "workspaces": [{ "id": "workspace", "label": "Workspace", "path": "project" }]
    }
    """;

    private static JsonObject Candidate(string existing) => ConfigurationMigration.BuildCandidate(
        existing,
        new SetupConfigurationInput("current-codex.cmd", "https://device.example.ts.net", null),
        new SetupConfigurationDefaults("default-data", "desktop-home"));

    private static string Sha256(string value) => Convert.ToHexString(
        SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLowerInvariant();

    private static async Task ValidateJsonAsync(string path, CancellationToken cancellationToken)
    {
        _ = JsonNode.Parse(await File.ReadAllTextAsync(path, cancellationToken))
            ?? throw new InvalidOperationException("invalid test configuration");
    }
}
