using System.Collections;
using System.Text;
using System.Text.Json.Nodes;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;

public sealed class LegacyFixedActionCatalogTests
{
    [Fact]
    public void EmbeddedCatalog_HasApprovedIdentityCountsAndOrder()
    {
        var catalog = LegacyFixedActionCatalog.Default;

        Assert.Equal(LegacyFixedActionCatalog.ExpectedDocumentSha256, catalog.DocumentSha256);
        Assert.Equal(
            LegacyFixedActionCatalog.ExpectedGeneratorSourceSha256,
            catalog.GeneratorSourceSha256);
        Assert.Equal(328, catalog.Actions.Count);
        Assert.Equal(Enumerable.Range(1, 328).Select(index => $"fixed-{index:000}"),
            catalog.Actions.Select(action => action.Id));
        Assert.Equal(78, catalog.GetActions(LegacyFixedMarket.Overseas).Count);
        Assert.Equal(46, catalog.GetActions(LegacyFixedMarket.Exchange).Count);
        Assert.Equal(204, catalog.GetActions(LegacyFixedMarket.Index).Count);
        Assert.Equal(322, catalog.Actions.Count(action => action.Available));
        Assert.Equal(6, catalog.Actions.Count(action => !action.Available));
    }

    [Fact]
    public void EmbeddedCatalog_ClosesEverySceneIdentityAgainstCoreCatalog()
    {
        foreach (var action in LegacyFixedActionCatalog.Default.Actions)
        {
            var definition = Assert.Single(
                LegacySceneCatalog.Definitions,
                candidate => candidate.BuilderKey == action.BuilderKey);
            Assert.Single(action.Aliases);
            Assert.Equal(action.Code, action.Aliases[0]);
            Assert.Contains(action.Code, definition.CutAliases);
            Assert.Contains(
                LegacySceneCatalog.FindByCutAlias(action.Code),
                candidate => candidate.BuilderKey == action.BuilderKey);
        }
    }

    [Fact]
    public void RuntimeAssets_AreApprovedAndImmutable()
    {
        var catalog = LegacyFixedActionCatalog.Default;

        Assert.Equal(
            "bin/Debug/Res/해외.ini",
            catalog.GetRuntimeAsset(LegacyFixedMarket.Overseas).RelativePath);
        Assert.Equal(
            "DBDC4EAC28BED9FD0DEABED7FE880F21CB49E48503FD3CCB95C0A29128818B2A",
            catalog.GetRuntimeAsset(LegacyFixedMarket.Overseas).Sha256);
        Assert.Equal(
            "bin/Debug/Res/환율.ini",
            catalog.GetRuntimeAsset(LegacyFixedMarket.Exchange).RelativePath);
        Assert.Equal(
            "bin/Debug/Res/지수.ini",
            catalog.GetRuntimeAsset(LegacyFixedMarket.Index).RelativePath);

        var dictionary = Assert.IsAssignableFrom<IDictionary>(catalog.RuntimeAssets);
        Assert.True(dictionary.IsReadOnly);
        Assert.Throws<NotSupportedException>(() =>
            dictionary.Add(
                LegacyFixedMarket.Overseas,
                new LegacyFixedRuntimeAsset(LegacyFixedMarket.Overseas, "bad", "bad")));
    }

    [Fact]
    public void QueryApi_ReturnsImmutableMarketAndSectionViews()
    {
        var catalog = LegacyFixedActionCatalog.Default;
        var overseas = catalog.GetActions(LegacyFixedMarket.Overseas);
        var sections = catalog.GetSections(LegacyFixedMarket.Overseas);
        var firstSection = sections[0];

        Assert.Equal(LegacyFixedMarket.Overseas, firstSection.Market);
        Assert.Equal("1열판기본", firstSection.Name);
        Assert.Equal(43, firstSection.Actions.Count);
        Assert.Same(catalog.Actions[0], catalog.GetAction("fixed-001"));
        Assert.Same(
            firstSection.Actions[0],
            catalog.GetActions(LegacyFixedMarket.Overseas, "1열판기본")[0]);
        Assert.Empty(catalog.GetActions(LegacyFixedMarket.Overseas, "없는 섹션"));

        var mutableActions = Assert.IsAssignableFrom<IList<LegacyFixedAction>>(overseas);
        Assert.True(mutableActions.IsReadOnly);
        Assert.Throws<NotSupportedException>(() => mutableActions.RemoveAt(0));
        Assert.Null(catalog.FindAction(null));
        Assert.Null(catalog.FindAction("fixed-999"));
        Assert.Throws<KeyNotFoundException>(() => catalog.GetAction("fixed-999"));
        Assert.Throws<ArgumentOutOfRangeException>(() =>
            catalog.GetActions((LegacyFixedMarket)99));
    }

    [Fact]
    public void Materialize_ComputesDateFromTrustedHostClock()
    {
        var now = new DateTimeOffset(2026, 7, 15, 23, 58, 0, TimeSpan.FromHours(9));

        var result = LegacyFixedActionCatalog.Default.Materialize("fixed-248", now);

        Assert.Equal("2026-07-15", result.Selection.DataCode);
        Assert.Equal("", result.Selection.GraphicType);
        Assert.Equal("$TODAY", result.Action.Selection.DataCode);
        Assert.Equal(LegacyFixedMarket.Index, result.RuntimeAsset.Market);
    }

    [Theory]
    [InlineData(0, "PRE_MARKET")]
    [InlineData(12, "PRE_MARKET")]
    [InlineData(13, "AFTER_MARKET")]
    [InlineData(23, "AFTER_MARKET")]
    public void Materialize_ComputesNxtSessionFromTrustedHostClock(int hour, string expected)
    {
        var now = new DateTimeOffset(2026, 7, 15, hour, 59, 0, TimeSpan.FromHours(9));

        var result = LegacyFixedActionCatalog.Default.Materialize("fixed-263", now);

        Assert.Equal(expected, result.Selection.GraphicType);
        Assert.Equal("$NXT_SESSION", result.Action.Selection.GraphicType);
        Assert.Equal("", result.Selection.DataCode);
    }

    [Fact]
    public void Materialize_LeavesMovingAverageStateOutOfTheActionIdentity()
    {
        var result = LegacyFixedActionCatalog.Default.Materialize(
            "fixed-136",
            new DateTimeOffset(2026, 7, 15, 9, 0, 0, TimeSpan.FromHours(9)));

        Assert.Equal(string.Empty, result.Selection.GraphicType);
        Assert.Equal("", result.Action.Selection.GraphicType);
    }

    [Fact]
    public void Materialize_RetainsGraphicTypeForOtherBuilders()
    {
        var result = LegacyFixedActionCatalog.Default.Materialize(
            "fixed-001",
            DateTimeOffset.Now);

        Assert.Equal("1열판기본", result.Selection.GraphicType);
    }

    [Fact]
    public void Materialize_ResolvesEveryTemplateWithoutAcceptingSelectionOverrides()
    {
        var catalog = LegacyFixedActionCatalog.Default;
        var beforeBoundary = new DateTimeOffset(
            2026, 7, 15, 12, 59, 0, TimeSpan.FromHours(9));
        var afterBoundary = beforeBoundary.AddHours(1);

        foreach (var action in catalog.Actions.Where(action => action.Available))
        {
            var before = catalog.Materialize(
                action.Id,
                beforeBoundary);
            Assert.DoesNotContain('$', before.Selection.GroupCode);
            Assert.DoesNotContain('$', before.Selection.Subject);
            Assert.DoesNotContain('$', before.Selection.GraphicType);
            Assert.DoesNotContain('$', before.Selection.Subtype);
            Assert.DoesNotContain('$', before.Selection.DataCode);

            if (action.DynamicDate)
            {
                Assert.Equal("2026-07-15", before.Selection.DataCode);
            }
            else if (action.DynamicNxtSession)
            {
                Assert.Equal("PRE_MARKET", before.Selection.GraphicType);
                Assert.Equal(
                    "AFTER_MARKET",
                    catalog.Materialize(action.Id, afterBoundary).Selection.GraphicType);
            }
            else if (action.SupportsMovingAverages)
            {
                Assert.Equal(action.Selection.GraphicType, before.Selection.GraphicType);
            }
            else
            {
                Assert.Equal(action.Selection, before.Selection);
            }
        }

        Assert.Equal(50, catalog.Actions.Count(action => action.DynamicDate));
        Assert.Equal(28, catalog.Actions.Count(action => action.DynamicNxtSession));
        Assert.Equal(74, catalog.Actions.Count(action => action.SupportsMovingAverages));
    }

    [Fact]
    public void Materialize_RejectsUnknownAndUnavailableActions()
    {
        var catalog = LegacyFixedActionCatalog.Default;

        Assert.Throws<KeyNotFoundException>(() =>
            catalog.Materialize("fixed-999", DateTimeOffset.Now));
        var exception = Assert.Throws<InvalidOperationException>(() =>
            catalog.Materialize("fixed-245", DateTimeOffset.Now));
        Assert.Contains("수동 순매도", exception.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Loader_RejectsUnknownJsonMembersAndMissingRequiredMembers()
    {
        var unknown = ReadDocument();
        unknown["unexpected"] = true;
        AssertInvalid(unknown, "JSON schema");

        var nestedUnknown = ReadDocument();
        Action(nestedUnknown, 0)["selection"]!["webOverride"] = "5006";
        AssertInvalid(nestedUnknown, "JSON schema");

        var missing = ReadDocument();
        missing.AsObject().Remove("schemaVersion");
        AssertInvalid(missing, "JSON schema");

        var nullActions = ReadDocument();
        nullActions["actions"] = null;
        AssertInvalid(nullActions, "actions is required");
    }

    [Fact]
    public void Loader_RejectsDuplicateJsonMembers()
    {
        var original = Encoding.UTF8.GetString(ReadEmbeddedBytes());
        var duplicate = original.Replace(
            "\"schemaVersion\": 1,",
            "\"schemaVersion\": 1,\n  \"schemaVersion\": 1,",
            StringComparison.Ordinal);

        var exception = Assert.Throws<InvalidDataException>(() =>
            LegacyFixedActionCatalog.LoadForValidation(Encoding.UTF8.GetBytes(duplicate)));

        Assert.Contains("JSON schema", exception.Message, StringComparison.Ordinal);
        Assert.IsType<System.Text.Json.JsonException>(exception.InnerException);
    }

    [Fact]
    public void Loader_RejectsTamperedDocumentHash()
    {
        var tampered = ReadEmbeddedBytes().Concat([(byte)' ']).ToArray();

        var exception = Assert.Throws<InvalidDataException>(() =>
            LegacyFixedActionCatalog.LoadForValidation(
                tampered,
                LegacyFixedActionCatalog.ExpectedDocumentSha256));

        Assert.Contains("hash is invalid", exception.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Loader_RejectsUnknownBuilderCodeAndAlias()
    {
        var unknownBuilder = ReadDocument();
        Action(unknownBuilder, 0)["builderKey"] = "s9999";
        AssertInvalid(unknownBuilder, "unknown builder");

        var wrongCode = ReadDocument();
        Action(wrongCode, 0)["code"] = "5006";
        AssertInvalid(wrongCode, "is not an alias");

        var wrongAlias = ReadDocument();
        Action(wrongAlias, 0)["aliases"] = new JsonArray("N5001");
        AssertInvalid(wrongAlias, "sole alias");
    }

    [Fact]
    public void Loader_RejectsNonSequentialIdAndSelectionPlaceholders()
    {
        var wrongId = ReadDocument();
        Action(wrongId, 0)["id"] = "fixed-002";
        AssertInvalid(wrongId, "action id at index 0");

        var untrustedSelection = ReadDocument();
        Action(untrustedSelection, 0)["selection"]!["graphicType"] = "$WEB_VALUE";
        AssertInvalid(untrustedSelection, "unapproved selection placeholder");

        var badDynamicDate = ReadDocument();
        Action(badDynamicDate, 247)["dynamicDate"] = false;
        AssertInvalid(badDynamicDate, "dynamic-date declaration");
    }

    [Fact]
    public void Loader_RejectsTamperedRuntimeAssetMetadata()
    {
        var path = ReadDocument();
        path["runtimeAssets"]!["overseas"]!["relativePath"] = "C:/untrusted/해외.ini";
        AssertInvalid(path, "runtime asset metadata");

        var hash = ReadDocument();
        hash["runtimeAssets"]!["index"]!["sha256"] = new string('0', 64);
        AssertInvalid(hash, "runtime asset metadata");
    }

    private static JsonNode ReadDocument() =>
        JsonNode.Parse(ReadEmbeddedBytes()) ?? throw new InvalidOperationException();

    private static byte[] ReadEmbeddedBytes()
    {
        using var stream = typeof(LegacyFixedActionCatalog).Assembly.GetManifestResourceStream(
            "MBN_STOCK_WEBVIEW.LegacyApplication.Catalog.LegacyFixedActions.json")
            ?? throw new InvalidOperationException("Embedded test catalog was not found.");
        using var buffer = new MemoryStream();
        stream.CopyTo(buffer);
        return buffer.ToArray();
    }

    private static JsonObject Action(JsonNode document, int index) =>
        document["actions"]!.AsArray()[index]!.AsObject();

    private static void AssertInvalid(JsonNode document, string expectedMessage)
    {
        var bytes = Encoding.UTF8.GetBytes(document.ToJsonString());
        var exception = Assert.Throws<InvalidDataException>(() =>
            LegacyFixedActionCatalog.LoadForValidation(bytes));
        Assert.Contains(expectedMessage, exception.Message, StringComparison.OrdinalIgnoreCase);
    }
}
