using System.Collections.Concurrent;
using System.Reflection;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Nexus.Api.Controllers;
using Nexus.Api.Models;
using Nexus.Api.Services;
using Xunit;

namespace Nexus.Api.Tests;

public sealed class OpenClawContentServicesTests
{
    [Fact]
    public async Task Memory_uses_agent_files_and_workspace_rpc_with_source_metadata()
    {
        var gateway = new ContentConfigurationStub();
        gateway.AgentFiles =
        [
            new OpenClawAgentFileSummaryDto(
                "MEMORY.md",
                false,
                15,
                DateTimeOffset.UtcNow,
                "hash")
        ];
        gateway.Listings["memory"] =
        [
            Entry("memory/2026-07-31.md", 24)
        ];
        gateway.AgentFileContent = "Long term memory";
        gateway.Files["memory/2026-07-31.md"] =
            File("bao-agent", "memory/2026-07-31.md", "Daily memory needle");
        var service = new MemoryService(gateway);

        var listed = await service.GetAllAsync("bao-agent");
        var found = await service.SearchAsync("needle", "bao-agent");
        var longTerm = await service.GetFileAsync("MEMORY.md", "bao-agent");

        Assert.Collection(
            listed,
            item =>
            {
                Assert.Equal("MEMORY.md", item.WorkspacePath);
                Assert.Equal("bao-agent", item.SourceAgentId);
            },
            item =>
            {
                Assert.Equal("memory/2026-07-31.md", item.WorkspacePath);
                Assert.Equal("2026-07-31.md", item.Path);
            });
        Assert.Equal(
            "memory/2026-07-31.md",
            Assert.Single(found).WorkspacePath);
        Assert.Equal("MEMORY.md", longTerm?.WorkspacePath);
        Assert.DoesNotContain(
            listed.Select(item => item.WorkspacePath),
            path => path.StartsWith('/'));
    }

    [Fact]
    public async Task Memory_search_limits_live_file_reads_to_four()
    {
        var gateway = new ContentConfigurationStub
        {
            ReadDelay = TimeSpan.FromMilliseconds(20)
        };
        gateway.Listings["memory"] = Enumerable.Range(1, 12)
            .Select(index => Entry($"memory/{index:00}.md", 20))
            .ToArray();
        foreach (var entry in gateway.Listings["memory"])
        {
            gateway.Files[entry.Path] =
                File("iris", entry.Path, $"needle {entry.Name}");
        }

        var results = await new MemoryService(gateway)
            .SearchAsync("needle", "iris");

        Assert.Equal(12, results.Count);
        Assert.InRange(gateway.MaxConcurrentReads, 1, 4);
    }

    [Fact]
    public async Task Missing_daily_memory_directory_preserves_memory_file()
    {
        var gateway = new ContentConfigurationStub
        {
            MissingMemoryDirectory = true,
            AgentFileContent = "Long term needle"
        };
        gateway.AgentFiles =
        [
            new OpenClawAgentFileSummaryDto(
                "MEMORY.md",
                false,
                16,
                DateTimeOffset.UtcNow,
                "hash")
        ];
        var service = new MemoryService(gateway);

        var listed = await service.GetAllAsync("iris");
        var searched = await service.SearchAsync("needle", "iris");

        Assert.Equal("MEMORY.md", Assert.Single(listed).WorkspacePath);
        Assert.Equal("MEMORY.md", Assert.Single(searched).WorkspacePath);
    }

    [Fact]
    public async Task Docs_and_incidents_use_workspace_relative_rpc_paths()
    {
        var gateway = new ContentConfigurationStub();
        gateway.Listings[""] = [Entry("README.md", 30)];
        gateway.Listings["nexus-phases"] =
            [Entry("nexus-phases/phase-1.md", 30)];
        gateway.Listings["skills"] = [];
        gateway.Listings["nexus"] = [];
        gateway.Listings["nexus/phases"] = [];
        gateway.Listings["memory/incidents"] =
            [Entry("memory/incidents/2026-07-31-gateway.md", 90)];
        gateway.Files["README.md"] =
            File("iris", "README.md", "# Nexus");
        gateway.Files["memory/incidents/2026-07-31-gateway.md"] =
            File(
                "iris",
                "memory/incidents/2026-07-31-gateway.md",
                "# Gateway outage\n**Severity:** high\n\nRecovered.");

        var docs = await new DocService(gateway).GetAllAsync("iris");
        var incidents = await new IncidentService(gateway).GetAllAsync("iris");

        Assert.Contains(
            docs,
            item => item.WorkspacePath == "README.md"
                    && item.SourceAgentId == "iris");
        var incident = Assert.Single(incidents);
        Assert.Equal("high", incident.Severity);
        Assert.Equal(
            "memory/incidents/2026-07-31-gateway.md",
            incident.WorkspacePath);
    }

    [Fact]
    public void Sensitive_content_controllers_are_owner_only()
    {
        Type[] controllers =
        [
            typeof(MemoryController),
            typeof(DocsController),
            typeof(IncidentsController)
        ];

        foreach (var controller in controllers)
        {
            var authorize = Assert.Single(
                controller.GetCustomAttributes<AuthorizeAttribute>());
            Assert.Equal("owner", authorize.Roles);
        }
    }

    [Fact]
    public async Task Adapter_reports_gateway_unavailable_instead_of_empty_success()
    {
        var controller = new MemoryController(new UnavailableMemoryService());
        var result = await controller.GetAll();

        Assert.Equal(
            StatusCodes.Status503ServiceUnavailable,
            Assert.IsAssignableFrom<IStatusCodeHttpResult>(result)
                .StatusCode);
    }

    private static OpenClawWorkspaceEntryDto Entry(string path, long size)
        => new(
            path,
            Path.GetFileName(path),
            "file",
            size,
            DateTimeOffset.UtcNow);

    private static OpenClawWorkspaceFileDto File(
        string agentId,
        string path,
        string content)
        => new(
            agentId,
            path,
            Path.GetFileName(path),
            System.Text.Encoding.UTF8.GetByteCount(content),
            DateTimeOffset.UtcNow,
            "text/markdown",
            "utf8",
            content,
            "hash",
            DateTimeOffset.UtcNow);

    private sealed class ContentConfigurationStub
        : IOpenClawAgentConfigurationService
    {
        private int activeReads;
        private int maxConcurrentReads;

        public IReadOnlyList<OpenClawAgentFileSummaryDto> AgentFiles { get; set; }
            = [];
        public string AgentFileContent { get; set; } = string.Empty;
        public ConcurrentDictionary<
            string,
            IReadOnlyList<OpenClawWorkspaceEntryDto>> Listings { get; } =
            new(StringComparer.Ordinal);
        public ConcurrentDictionary<string, OpenClawWorkspaceFileDto> Files { get; }
            = new(StringComparer.Ordinal);
        public TimeSpan ReadDelay { get; set; }
        public bool MissingMemoryDirectory { get; set; }
        public int MaxConcurrentReads => Volatile.Read(ref maxConcurrentReads);

        public Task<OpenClawAgentFileCollectionDto> GetAgentFilesAsync(
            string agentId,
            CancellationToken cancellationToken = default)
            => Task.FromResult(new OpenClawAgentFileCollectionDto(
                agentId,
                AgentFiles,
                DateTimeOffset.UtcNow));

        public Task<OpenClawAgentFileDto> GetAgentFileAsync(
            string agentId,
            string fileName,
            CancellationToken cancellationToken = default)
            => Task.FromResult(new OpenClawAgentFileDto(
                agentId,
                fileName,
                false,
                System.Text.Encoding.UTF8.GetByteCount(AgentFileContent),
                DateTimeOffset.UtcNow,
                AgentFileContent,
                "hash",
                DateTimeOffset.UtcNow));

        public Task<OpenClawWorkspaceCollectionDto> GetWorkspaceAsync(
            string agentId,
            string? path,
            int offset,
            int limit,
            CancellationToken cancellationToken = default)
        {
            var normalized = path ?? string.Empty;
            if (MissingMemoryDirectory
                && string.Equals(normalized, "memory", StringComparison.Ordinal))
            {
                throw new OpenClawGatewayRpcException(
                    "PATH_NOT_FOUND",
                    "Optional memory directory is absent.");
            }
            Listings.TryGetValue(normalized, out var entries);
            entries ??= [];
            return Task.FromResult(new OpenClawWorkspaceCollectionDto(
                agentId,
                normalized,
                null,
                entries.Take(limit).ToArray(),
                entries.Count,
                offset,
                DateTimeOffset.UtcNow));
        }

        public async Task<OpenClawWorkspaceFileDto> GetWorkspaceFileAsync(
            string agentId,
            string path,
            CancellationToken cancellationToken = default)
        {
            var active = Interlocked.Increment(ref activeReads);
            UpdateMaximum(active);
            try
            {
                if (ReadDelay > TimeSpan.Zero)
                    await Task.Delay(ReadDelay, cancellationToken);
                return Files[path];
            }
            finally
            {
                Interlocked.Decrement(ref activeReads);
            }
        }

        private void UpdateMaximum(int value)
        {
            while (true)
            {
                var current = Volatile.Read(ref maxConcurrentReads);
                if (value <= current
                    || Interlocked.CompareExchange(
                        ref maxConcurrentReads,
                        value,
                        current) == current)
                {
                    return;
                }
            }
        }

        public Task<OpenClawAgentFileWriteDto> SetAgentFileAsync(
            string agentId,
            string fileName,
            UpdateOpenClawAgentFileRequest request,
            OpenClawInvocationContext invocationContext,
            CancellationToken cancellationToken = default)
            => throw new NotSupportedException();

        public Task<OpenClawConfigSchemaLookupDto> GetConfigSchemaAsync(
            string path,
            CancellationToken cancellationToken = default)
            => throw new NotSupportedException();

        public Task<OpenClawConfigSnapshotDto> GetConfigAsync(
            CancellationToken cancellationToken = default)
            => throw new NotSupportedException();

        public Task<OpenClawConfigPatchDto> PatchConfigAsync(
            PatchOpenClawConfigRequest request,
            OpenClawInvocationContext invocationContext,
            CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
    }

    private sealed class UnavailableMemoryService : IMemoryService
    {
        public Task<IReadOnlyList<MemoryFileInfo>> GetAllAsync(
            string agentId = "iris",
            CancellationToken cancellationToken = default)
            => throw new OpenClawAgentConfigurationUnavailableException(
                "disconnected",
                "agents.files.list",
                "operator.read",
                "Gateway unavailable.");

        public Task<IReadOnlyList<MemorySearchResult>> SearchAsync(
            string query,
            string agentId = "iris",
            CancellationToken cancellationToken = default)
            => throw new NotSupportedException();

        public Task<MemoryFileContent?> GetFileAsync(
            string name,
            string agentId = "iris",
            CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
    }
}
