using System.Text.Json;
using ZB.MOM.WW.MxGateway.Contracts;

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

public sealed class ParityFixtureMatrixTests
{
    /// <summary>Verifies that the parity matrix declares current protocol versions and comparison fields.</summary>
    [Fact]
    public void Matrix_DeclaresCurrentProtocolVersionsAndComparisonFields()
    {
        using JsonDocument matrix = LoadParityMatrix();
        JsonElement root = matrix.RootElement;

        Assert.Equal(1, root.GetProperty("schemaVersion").GetInt32());
        Assert.Equal("mxaccess-gateway-parity-fixture-matrix", root.GetProperty("fixtureSet").GetString());
        Assert.Equal(GatewayContractInfo.GatewayProtocolVersion, root.GetProperty("gatewayProtocolVersion").GetUInt32());
        Assert.Equal(GatewayContractInfo.WorkerProtocolVersion, root.GetProperty("workerProtocolVersion").GetUInt32());

        JsonElement comparisonFormat = root.GetProperty("comparisonFormat");
        AssertRequiredFields(
            comparisonFormat.GetProperty("directMxAccess").GetProperty("requiredFields"),
            "method",
            "arguments",
            "returnedValue",
            "hresult",
            "statuses",
            "events");
        AssertRequiredFields(
            comparisonFormat.GetProperty("gatewayResult").GetProperty("requiredFields"),
            "kind",
            "protocolStatus",
            "returnValue",
            "hresult",
            "statuses",
            "events");
        AssertRequiredFields(
            comparisonFormat.GetProperty("eventFields"),
            "family",
            "value",
            "quality",
            "sourceTimestamp",
            "statuses",
            "workerSequence");
        AssertRequiredFields(
            comparisonFormat.GetProperty("comparisonKeys"),
            "hresult",
            "statusArrayShape",
            "statusRawFields",
            "eventFamilyOrder",
            "eventPayloadShape",
            "valueProjection",
            "rawFallbackMetadata");
    }

    /// <summary>Verifies that the parity matrix covers every public MXAccess method.</summary>
    [Fact]
    public void Matrix_CoversEveryPublicMxAccessMethod()
    {
        using JsonDocument matrix = LoadParityMatrix();
        JsonElement methodFixtures = matrix.RootElement.GetProperty("methodFixtures");

        Dictionary<string, JsonElement> fixturesByMethod = [];
        HashSet<string> ids = new(StringComparer.Ordinal);

        foreach (JsonElement fixture in methodFixtures.EnumerateArray())
        {
            string id = fixture.GetProperty("id").GetString()!;
            string method = fixture.GetProperty("method").GetString()!;
            string commandKind = fixture.GetProperty("commandKind").GetString()!;
            string status = fixture.GetProperty("status").GetString()!;

            Assert.True(ids.Add(id), $"Duplicate parity fixture id '{id}'.");
            Assert.True(fixturesByMethod.TryAdd(method, fixture), $"Duplicate parity method '{method}'.");
            Assert.StartsWith("MX_COMMAND_KIND_", commandKind, StringComparison.Ordinal);
            Assert.Contains(status, KnownFixtureStatuses);
            Assert.NotEmpty(fixture.GetProperty("assertions").EnumerateArray());
            AssertCaptureReferencesAreRelative(fixture.GetProperty("captureReferences"));
        }

        Assert.Equal(ExpectedPublicMethods.Order(StringComparer.Ordinal), fixturesByMethod.Keys.Order(StringComparer.Ordinal));

        foreach (string method in ExpectedPublicMethods)
        {
            JsonElement fixture = fixturesByMethod[method];
            string status = fixture.GetProperty("status").GetString()!;

            Assert.True(
                status == "planned_fixture" || status == "documented_gap",
                $"Method '{method}' must have a planned parity fixture or documented gap.");
        }
    }

    /// <summary>Verifies that the parity matrix covers required scenario groups.</summary>
    [Fact]
    public void Matrix_CoversRequiredParityScenarioGroups()
    {
        using JsonDocument matrix = LoadParityMatrix();
        HashSet<string> knownFixtureIds = GetFixtureIds(matrix.RootElement);
        Dictionary<string, JsonElement> groupsById = [];

        foreach (JsonElement group in matrix.RootElement.GetProperty("scenarioGroups").EnumerateArray())
        {
            string id = group.GetProperty("id").GetString()!;

            Assert.True(groupsById.TryAdd(id, group), $"Duplicate parity scenario group '{id}'.");
            Assert.NotEmpty(group.GetProperty("description").GetString()!);
            Assert.NotEmpty(group.GetProperty("fixtureIds").EnumerateArray());
            AssertCaptureReferencesAreRelative(group.GetProperty("captureReferences"));

            foreach (JsonElement fixtureIdElement in group.GetProperty("fixtureIds").EnumerateArray())
            {
                string fixtureId = fixtureIdElement.GetString()!;
                Assert.Contains(fixtureId, knownFixtureIds);
            }
        }

        foreach (string requiredGroup in RequiredScenarioGroups)
        {
            Assert.True(groupsById.ContainsKey(requiredGroup), $"Missing required parity scenario group '{requiredGroup}'.");
        }

        AssertScenarioCovers(groupsById["invalid_handles"], "method.remove-item.basic", "method.write.value-status-matrix");
        AssertScenarioCovers(groupsById["write_statuses"], "method.write.value-status-matrix", "event.on-write-complete.status");
        AssertScenarioCovers(groupsById["secured_writes"], "method.write-secured.rejection-gap", "method.write-secured2.authenticated");
        AssertScenarioCovers(groupsById["add_item_context"], "method.add-item2.context", "method.add-buffered-item.context");
        AssertScenarioCovers(groupsById["buffered_registration"], "method.add-buffered-item.context", "event.on-buffered-data-change.batch-gap");
    }

    /// <summary>Verifies that the parity matrix covers every public MXAccess event family.</summary>
    [Fact]
    public void Matrix_CoversEveryPublicMxAccessEventFamily()
    {
        using JsonDocument matrix = LoadParityMatrix();
        Dictionary<string, JsonElement> fixturesByFamily = [];

        foreach (JsonElement fixture in matrix.RootElement.GetProperty("eventFixtures").EnumerateArray())
        {
            string family = fixture.GetProperty("family").GetString()!;
            string status = fixture.GetProperty("status").GetString()!;

            Assert.True(fixturesByFamily.TryAdd(family, fixture), $"Duplicate parity event family '{family}'.");
            Assert.Contains(status, KnownFixtureStatuses);
            Assert.NotEmpty(fixture.GetProperty("assertions").EnumerateArray());
            AssertCaptureReferencesAreRelative(fixture.GetProperty("captureReferences"));
        }

        foreach (string eventFamily in ExpectedEventFamilies)
        {
            Assert.True(fixturesByFamily.ContainsKey(eventFamily), $"Missing parity fixture for event family '{eventFamily}'.");
        }

        Assert.Equal("documented_gap", fixturesByFamily["MX_EVENT_FAMILY_OPERATION_COMPLETE"].GetProperty("status").GetString());
        Assert.Equal("documented_gap", fixturesByFamily["MX_EVENT_FAMILY_ON_BUFFERED_DATA_CHANGE"].GetProperty("status").GetString());
    }

    private static readonly string[] ExpectedPublicMethods =
    [
        "Register",
        "Unregister",
        "AddItem",
        "AddItem2",
        "RemoveItem",
        "Advise",
        "UnAdvise",
        "AdviseSupervisory",
        "AddBufferedItem",
        "SetBufferedUpdateInterval",
        "Suspend",
        "Activate",
        "Write",
        "Write2",
        "WriteSecured",
        "WriteSecured2",
        "AuthenticateUser",
        "ArchestrAUserToId",
    ];

    private static readonly string[] ExpectedEventFamilies =
    [
        "MX_EVENT_FAMILY_ON_DATA_CHANGE",
        "MX_EVENT_FAMILY_ON_WRITE_COMPLETE",
        "MX_EVENT_FAMILY_OPERATION_COMPLETE",
        "MX_EVENT_FAMILY_ON_BUFFERED_DATA_CHANGE",
    ];

    private static readonly string[] RequiredScenarioGroups =
    [
        "invalid_handles",
        "write_statuses",
        "secured_writes",
        "add_item_context",
        "buffered_registration",
    ];

    private static readonly string[] KnownFixtureStatuses =
    [
        "planned_fixture",
        "documented_gap",
    ];

    private static void AssertRequiredFields(
        JsonElement fields,
        params string[] expectedFields)
    {
        HashSet<string> declared = fields
            .EnumerateArray()
            .Select(field => field.GetString()!)
            .ToHashSet(StringComparer.Ordinal);

        foreach (string expectedField in expectedFields)
        {
            Assert.Contains(expectedField, declared);
        }
    }

    private static void AssertCaptureReferencesAreRelative(JsonElement captureReferences)
    {
        int count = 0;

        foreach (JsonElement captureReference in captureReferences.EnumerateArray())
        {
            string path = captureReference.GetString()!;

            Assert.StartsWith("captures/", path, StringComparison.Ordinal);
            Assert.DoesNotContain("\\", path, StringComparison.Ordinal);
            Assert.False(Path.IsPathRooted(path), $"Capture reference '{path}' must be relative.");
            count++;
        }

        Assert.True(count > 0, "Each parity fixture must reference at least one MXAccess capture.");
    }

    private static void AssertScenarioCovers(
        JsonElement group,
        params string[] fixtureIds)
    {
        HashSet<string> declared = group
            .GetProperty("fixtureIds")
            .EnumerateArray()
            .Select(fixtureId => fixtureId.GetString()!)
            .ToHashSet(StringComparer.Ordinal);

        foreach (string fixtureId in fixtureIds)
        {
            Assert.Contains(fixtureId, declared);
        }
    }

    private static HashSet<string> GetFixtureIds(JsonElement root)
    {
        HashSet<string> ids = new(StringComparer.Ordinal);

        foreach (JsonElement fixture in root.GetProperty("methodFixtures").EnumerateArray())
        {
            ids.Add(fixture.GetProperty("id").GetString()!);
        }

        foreach (JsonElement fixture in root.GetProperty("eventFixtures").EnumerateArray())
        {
            ids.Add(fixture.GetProperty("id").GetString()!);
        }

        return ids;
    }

    private static JsonDocument LoadParityMatrix()
    {
        return JsonDocument.Parse(File.ReadAllText(Path.Combine(GetParityFixtureRoot().FullName, "parity-fixture-matrix.json")));
    }

    private static DirectoryInfo GetParityFixtureRoot()
    {
        DirectoryInfo repositoryRoot = FindRepositoryRoot();

        return new DirectoryInfo(Path.Combine(repositoryRoot.FullName, "clients", "proto", "fixtures", "parity"));
    }

    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.");
    }
}
