using System.Text.Json;
using Google.Protobuf;
using Google.Protobuf.Reflection;
using ZB.MOM.WW.MxGateway.Contracts;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;

namespace ZB.MOM.WW.MxGateway.Tests.Contracts;

public sealed class ClientProtoInputTests
{
    /// <summary>
    /// Guards the published client descriptor set against silent staleness. Every message,
    /// field, enum, enum value, service, and method compiled into the in-process contract
    /// (which the build regenerates from the current <c>.proto</c> sources) must appear in the
    /// committed protoset. A missing symbol means the descriptor was not regenerated after a
    /// proto change; run <c>scripts/publish-client-proto-inputs.ps1</c> and commit the
    /// refreshed protoset.
    /// The check is semantic (symbol presence) rather than byte-wise, so it is independent of protoc
    /// version and does not require protoc on the test runner.
    /// </summary>
    [Fact]
    public void Descriptor_ContainsEveryContractSymbol()
    {
        DirectoryInfo repositoryRoot = FindRepositoryRoot();
        string descriptorPath = Path.Combine(
            repositoryRoot.FullName,
            "clients",
            "proto",
            "descriptors",
            "mxaccessgw-client-v1.protoset");

        Assert.True(File.Exists(descriptorPath), $"Expected descriptor set '{descriptorPath}' to exist.");

        FileDescriptorSet descriptorSet = FileDescriptorSet.Parser.ParseFrom(File.ReadAllBytes(descriptorPath));

        HashSet<string> publishedMessages = new(StringComparer.Ordinal);
        HashSet<string> publishedFields = new(StringComparer.Ordinal);
        HashSet<string> publishedEnums = new(StringComparer.Ordinal);
        HashSet<string> publishedServices = new(StringComparer.Ordinal);
        foreach (FileDescriptorProto file in descriptorSet.File)
        {
            foreach (DescriptorProto message in file.MessageType)
            {
                CollectPublishedSymbols(file.Package, message, publishedMessages, publishedFields, publishedEnums);
            }

            foreach (EnumDescriptorProto enumType in file.EnumType)
            {
                CollectPublishedEnumSymbols(file.Package, enumType, publishedEnums);
            }

            foreach (ServiceDescriptorProto service in file.Service)
            {
                string serviceFullName = string.IsNullOrEmpty(file.Package) ? service.Name : file.Package + "." + service.Name;
                publishedServices.Add(serviceFullName);
                foreach (MethodDescriptorProto method in service.Method)
                {
                    publishedServices.Add(serviceFullName + "/" + method.Name);
                }
            }
        }

        List<string> missing = [];
        FileDescriptor[] contractFiles =
        [
            MxaccessGatewayReflection.Descriptor,
            MxaccessWorkerReflection.Descriptor,
            GalaxyRepositoryReflection.Descriptor,
        ];
        foreach (FileDescriptor file in contractFiles)
        {
            foreach (MessageDescriptor message in file.MessageTypes)
            {
                CollectMissingContractSymbols(message, publishedMessages, publishedFields, publishedEnums, missing);
            }

            foreach (EnumDescriptor enumType in file.EnumTypes)
            {
                CollectMissingEnumSymbols(enumType, publishedEnums, missing);
            }

            foreach (ServiceDescriptor service in file.Services)
            {
                if (!publishedServices.Contains(service.FullName))
                {
                    missing.Add(service.FullName);
                }

                foreach (MethodDescriptor method in service.Methods)
                {
                    string key = service.FullName + "/" + method.Name;
                    if (!publishedServices.Contains(key))
                    {
                        missing.Add(key);
                    }
                }
            }
        }

        Assert.True(
            missing.Count == 0,
            "Published client descriptor is stale; regenerate with scripts/publish-client-proto-inputs.ps1 and commit. "
                + "Missing symbols: "
                + string.Join(", ", missing));
    }

    private static void CollectPublishedSymbols(
        string package,
        DescriptorProto message,
        HashSet<string> messages,
        HashSet<string> fields,
        HashSet<string> enums)
    {
        string fullName = string.IsNullOrEmpty(package) ? message.Name : package + "." + message.Name;
        messages.Add(fullName);

        foreach (FieldDescriptorProto field in message.Field)
        {
            fields.Add(fullName + "/" + field.Name);
        }

        foreach (DescriptorProto nested in message.NestedType)
        {
            CollectPublishedSymbols(fullName, nested, messages, fields, enums);
        }

        foreach (EnumDescriptorProto enumType in message.EnumType)
        {
            CollectPublishedEnumSymbols(fullName, enumType, enums);
        }
    }

    private static void CollectPublishedEnumSymbols(
        string containingScope,
        EnumDescriptorProto enumType,
        HashSet<string> enums)
    {
        string enumFullName = string.IsNullOrEmpty(containingScope) ? enumType.Name : containingScope + "." + enumType.Name;
        enums.Add(enumFullName);

        foreach (EnumValueDescriptorProto value in enumType.Value)
        {
            enums.Add(enumFullName + "/" + value.Name);
        }
    }

    private static void CollectMissingContractSymbols(
        MessageDescriptor message,
        HashSet<string> publishedMessages,
        HashSet<string> publishedFields,
        HashSet<string> publishedEnums,
        List<string> missing)
    {
        if (!publishedMessages.Contains(message.FullName))
        {
            missing.Add(message.FullName);
        }

        foreach (FieldDescriptor field in message.Fields.InDeclarationOrder())
        {
            string key = message.FullName + "/" + field.Name;
            if (!publishedFields.Contains(key))
            {
                missing.Add(key);
            }
        }

        foreach (MessageDescriptor nested in message.NestedTypes)
        {
            CollectMissingContractSymbols(nested, publishedMessages, publishedFields, publishedEnums, missing);
        }

        foreach (EnumDescriptor enumType in message.EnumTypes)
        {
            CollectMissingEnumSymbols(enumType, publishedEnums, missing);
        }
    }

    private static void CollectMissingEnumSymbols(
        EnumDescriptor enumType,
        HashSet<string> publishedEnums,
        List<string> missing)
    {
        if (!publishedEnums.Contains(enumType.FullName))
        {
            missing.Add(enumType.FullName);
        }

        foreach (EnumValueDescriptor value in enumType.Values)
        {
            string key = enumType.FullName + "/" + value.Name;
            if (!publishedEnums.Contains(key))
            {
                missing.Add(key);
            }
        }
    }

    /// <summary>Verifies that the proto inputs manifest declares current protocol versions and existing source files.</summary>
    [Fact]
    public void Manifest_DeclaresCurrentProtocolVersionsAndExistingInputs()
    {
        DirectoryInfo repositoryRoot = FindRepositoryRoot();
        string manifestPath = Path.Combine(repositoryRoot.FullName, "clients", "proto", "proto-inputs.json");

        using JsonDocument manifest = JsonDocument.Parse(File.ReadAllText(manifestPath));
        JsonElement root = manifest.RootElement;

        Assert.Equal(1, root.GetProperty("schemaVersion").GetInt32());
        Assert.Equal(GatewayContractInfo.GatewayProtocolVersion, root.GetProperty("gatewayProtocolVersion").GetUInt32());
        Assert.Equal(GatewayContractInfo.WorkerProtocolVersion, root.GetProperty("workerProtocolVersion").GetUInt32());

        string protoRoot = Path.Combine(repositoryRoot.FullName, root.GetProperty("protoRoot").GetString()!);
        foreach (JsonElement sourceFile in root.GetProperty("sourceFiles").EnumerateArray())
        {
            string sourcePath = Path.Combine(protoRoot, sourceFile.GetProperty("path").GetString()!);
            Assert.True(File.Exists(sourcePath), $"Expected proto source file '{sourcePath}' to exist.");
        }

        foreach (JsonProperty output in root.GetProperty("generatedOutputs").EnumerateObject())
        {
            string outputPath = Path.Combine(repositoryRoot.FullName, output.Value.GetString()!);
            Assert.True(Directory.Exists(outputPath), $"Expected generated output directory '{outputPath}' to exist.");
        }
    }

    /// <summary>Verifies that the OpenSessionReply fixture parses with the current contract version.</summary>
    [Fact]
    public void OpenSessionReplyFixture_ParsesWithCurrentContract()
    {
        OpenSessionReply reply = ParseFixture(
            "open-session-reply.ok.json",
            OpenSessionReply.Parser);

        Assert.Equal(GatewayContractInfo.GatewayProtocolVersion, reply.GatewayProtocolVersion);
        Assert.Equal(GatewayContractInfo.WorkerProtocolVersion, reply.WorkerProtocolVersion);
        Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
    }

    /// <summary>Verifies that the RegisterCommand fixture parses with the current contract version.</summary>
    [Fact]
    public void RegisterCommandRequestFixture_ParsesWithCurrentContract()
    {
        MxCommandRequest request = ParseFixture(
            "register-command-request.json",
            MxCommandRequest.Parser);

        Assert.Equal(MxCommandKind.Register, request.Command.Kind);
        Assert.Equal("fixture-client", request.Command.Register.ClientName);
    }

    /// <summary>Verifies that the OnDataChange event fixture parses with the current contract version.</summary>
    [Fact]
    public void OnDataChangeEventFixture_ParsesWithCurrentContract()
    {
        MxEvent gatewayEvent = ParseFixture(
            "on-data-change-event.json",
            MxEvent.Parser);

        Assert.Equal(MxEventFamily.OnDataChange, gatewayEvent.Family);
        Assert.Equal(1ul, gatewayEvent.WorkerSequence);
        Assert.Equal(MxDataType.Integer, gatewayEvent.Value.DataType);
        Assert.Equal(MxEvent.BodyOneofCase.OnDataChange, gatewayEvent.BodyCase);
    }

    private static T ParseFixture<T>(
        string fixtureName,
        MessageParser<T> parser)
        where T : IMessage<T>
    {
        DirectoryInfo repositoryRoot = FindRepositoryRoot();
        string fixturePath = Path.Combine(repositoryRoot.FullName, "clients", "proto", "fixtures", "golden", fixtureName);

        return parser.ParseJson(File.ReadAllText(fixturePath));
    }

    private static DirectoryInfo FindRepositoryRoot()
    {
        DirectoryInfo? current = new(AppContext.BaseDirectory);

        while (current is not null)
        {
            if (File.Exists(Path.Combine(current.FullName, "CLAUDE.md"))
                && Directory.Exists(Path.Combine(current.FullName, "src"))
                && Directory.Exists(Path.Combine(current.FullName, "clients")))
            {
                return current;
            }

            current = current.Parent;
        }

        throw new DirectoryNotFoundException("Could not locate the repository root from the test output directory.");
    }
}
