using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Nexus.Api.Services;
using Xunit;

namespace Nexus.Api.Tests;

public sealed class OpenClawDeviceIdentityAndAuditTests
{
    private const string TraceParent =
        "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";

    [Fact]
    public async Task DeviceIdentity_IsStableSignsCanonicalPayloadAndPersistsDeviceToken()
    {
        var testDirectory = CreateTestDirectory();
        try
        {
            var statePath = Path.Combine(testDirectory, "device.json");
            var options = Options.Create(new GatewayConnectorOptions
            {
                DeviceStatePath = statePath
            });
            var firstStore = new OpenClawDeviceIdentityStore(
                options,
                NullLogger<OpenClawDeviceIdentityStore>.Instance);
            var firstIdentity = await firstStore.LoadOrCreateAsync();
            var payload = OpenClawGatewayProtocol.BuildDeviceAuthPayloadV3(
                firstIdentity.DeviceId,
                "gateway-client",
                "backend",
                "operator",
                ["operator.read"],
                1_737_264_000_000,
                "bootstrap-token",
                "nonce-1",
                "linux",
                "server");
            var signature = firstIdentity.Sign(payload);

            Assert.Equal(64, firstIdentity.DeviceId.Length);
            Assert.True(firstIdentity.Verify(payload, signature));

            await firstStore.StoreTokenAsync(
                firstIdentity.DeviceId,
                "operator",
                "gateway-binding-1",
                "paired-device-token",
                ["operator.read"]);

            var secondStore = new OpenClawDeviceIdentityStore(
                options,
                NullLogger<OpenClawDeviceIdentityStore>.Instance);
            var secondIdentity = await secondStore.LoadOrCreateAsync();
            var token = await secondStore.LoadTokenAsync(
                secondIdentity.DeviceId,
                "operator",
                "gateway-binding-1");
            var wrongGatewayToken = await secondStore.LoadTokenAsync(
                secondIdentity.DeviceId,
                "operator",
                "gateway-binding-2");

            Assert.Equal(firstIdentity.DeviceId, secondIdentity.DeviceId);
            Assert.Equal(firstIdentity.PublicKey, secondIdentity.PublicKey);
            Assert.NotNull(token);
            Assert.Equal("paired-device-token", token!.Token);
            Assert.Equal(["operator.read"], token.Scopes);
            Assert.Equal("gateway-binding-1", token.GatewayBinding);
            Assert.Null(wrongGatewayToken);

            Assert.True(await secondStore.RemoveTokenAsync(
                secondIdentity.DeviceId,
                "operator",
                "gateway-binding-1"));
            var detachedStore = new OpenClawDeviceIdentityStore(
                options,
                NullLogger<OpenClawDeviceIdentityStore>.Instance);
            Assert.Null(await detachedStore.LoadTokenAsync(
                secondIdentity.DeviceId,
                "operator",
                "gateway-binding-1"));
        }
        finally
        {
            DeleteTestDirectory(testDirectory);
        }
    }

    [Fact]
    public async Task DeviceIdentity_CorruptStateFailsClosedInsteadOfRotatingIdentity()
    {
        var testDirectory = CreateTestDirectory();
        try
        {
            var statePath = Path.Combine(testDirectory, "device.json");
            await File.WriteAllTextAsync(statePath, """{ "schemaVersion": 1, "deviceId": "wrong" }""");
            var store = new OpenClawDeviceIdentityStore(
                Options.Create(new GatewayConnectorOptions { DeviceStatePath = statePath }),
                NullLogger<OpenClawDeviceIdentityStore>.Instance);

            await Assert.ThrowsAsync<InvalidDataException>(() => store.LoadOrCreateAsync());

            var unchanged = await File.ReadAllTextAsync(statePath);
            Assert.Contains("\"wrong\"", unchanged, StringComparison.Ordinal);
        }
        finally
        {
            DeleteTestDirectory(testDirectory);
        }
    }

    [Fact]
    public async Task OperationAudit_ReplaysCompletedKeyAcrossStoreRestartWithoutRawKey()
    {
        var testDirectory = CreateTestDirectory();
        try
        {
            var auditPath = Path.Combine(testDirectory, "operations.jsonl");
            var options = Options.Create(new GatewayConnectorOptions
            {
                OperationAuditPath = auditPath,
                DeviceStatePath = Path.Combine(testDirectory, "device.json")
            });
            var context = OpenClawInvocationContext.Create(
                actor: "owner-1",
                idempotencyKey: "client-secret-shaped-idempotency-value",
                correlationId: "corr-1",
                traceParent: TraceParent);
            var operation = new OpenClawOperationDescriptor(
                "tasks.cancel",
                "task",
                "task-1",
                OpenClawInvocationContextFactory.Hash("tasks.cancel|task-1|reason"));

            var firstStore = new OpenClawOperationAuditStore(options);
            var firstClaim = await firstStore.ClaimAsync(context, operation);
            Assert.Equal(OpenClawOperationClaimDisposition.Started, firstClaim.Disposition);
            await firstStore.CompleteAsync(
                context,
                operation,
                ok: true,
                state: "completed",
                message: "Task cancelled.");

            var restartedStore = new OpenClawOperationAuditStore(options);
            var replay = await restartedStore.ClaimAsync(context, operation);

            Assert.Equal(OpenClawOperationClaimDisposition.Replayed, replay.Disposition);
            Assert.True(replay.PreviousOk);
            var persisted = await File.ReadAllTextAsync(auditPath);
            Assert.DoesNotContain(context.IdempotencyKey, persisted, StringComparison.Ordinal);
            Assert.DoesNotContain("token", persisted, StringComparison.OrdinalIgnoreCase);
        }
        finally
        {
            DeleteTestDirectory(testDirectory);
        }
    }

    [Fact]
    public async Task OperationAudit_RejectsKeyReuseForDifferentIntentAndBlocksInDoubtRetry()
    {
        var testDirectory = CreateTestDirectory();
        try
        {
            var options = Options.Create(new GatewayConnectorOptions
            {
                OperationAuditPath = Path.Combine(testDirectory, "operations.jsonl"),
                DeviceStatePath = Path.Combine(testDirectory, "device.json")
            });
            var context = OpenClawInvocationContext.Create(
                idempotencyKey: "idem-1",
                correlationId: "corr-1",
                traceParent: TraceParent);
            var first = new OpenClawOperationDescriptor(
                "cron.run",
                "cron-job",
                "job-1",
                OpenClawInvocationContextFactory.Hash("force"));
            var conflict = first with
            {
                IntentFingerprint = OpenClawInvocationContextFactory.Hash("different")
            };

            var store = new OpenClawOperationAuditStore(options);
            Assert.Equal(
                OpenClawOperationClaimDisposition.Started,
                (await store.ClaimAsync(context, first)).Disposition);
            Assert.Equal(
                OpenClawOperationClaimDisposition.Conflict,
                (await store.ClaimAsync(context, conflict)).Disposition);

            var restartedStore = new OpenClawOperationAuditStore(options);
            Assert.Equal(
                OpenClawOperationClaimDisposition.InDoubt,
                (await restartedStore.ClaimAsync(context, first)).Disposition);
        }
        finally
        {
            DeleteTestDirectory(testDirectory);
        }
    }

    private static string CreateTestDirectory()
    {
        var root = Path.Combine(Path.GetTempPath(), "nexus-openclaw-tests");
        Directory.CreateDirectory(root);
        var path = Path.Combine(root, Guid.NewGuid().ToString("N"));
        Directory.CreateDirectory(path);
        return path;
    }

    private static void DeleteTestDirectory(string path)
    {
        var root = Path.GetFullPath(Path.Combine(Path.GetTempPath(), "nexus-openclaw-tests"));
        var resolved = Path.GetFullPath(path);
        if (!resolved.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.Ordinal))
            throw new InvalidOperationException("Refusing to delete a test directory outside the expected root.");
        if (Directory.Exists(resolved))
            Directory.Delete(resolved, recursive: true);
    }
}
