// Copyright 2023-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Shouldly;
using ZB.MOM.NatsNet.Server.Internal;

namespace ZB.MOM.NatsNet.Server.Tests.Internal;

/// <summary>
/// Tests for <see cref="SubjectTransform"/>.
/// Mirrors server/subject_transform_test.go:
///   TestPlaceHolderIndex (ID 2958), TestSubjectTransformHelpers (ID 2959),
///   TestSubjectTransforms (ID 2960),
///   TestSubjectTransformDoesntPanicTransformingMissingToken (ID 2961).
/// </summary>
public sealed class SubjectTransformTests
{
    [Fact]
    public void PlaceHolderIndex_ShouldParseAllFunctionTypes()
    {
        // Mirror: TestPlaceHolderIndex

        // $1 — old style
        var (tt, idxs, intArg, _, err) = SubjectTransform.IndexPlaceHolders("$1");
        err.ShouldBeNull();
        tt.ShouldBe(TransformType.Wildcard);
        idxs.Length.ShouldBe(1);
        idxs[0].ShouldBe(1);
        intArg.ShouldBe(-1);

        // {{partition(10,1,2,3)}}
        (tt, idxs, intArg, _, err) = SubjectTransform.IndexPlaceHolders("{{partition(10,1,2,3)}}");
        err.ShouldBeNull();
        tt.ShouldBe(TransformType.Partition);
        idxs.ShouldBe([1, 2, 3]);
        intArg.ShouldBe(10);

        // {{ Partition (10,1,2,3) }} (with spaces)
        (tt, idxs, intArg, _, err) = SubjectTransform.IndexPlaceHolders("{{ Partition (10,1,2,3) }}");
        err.ShouldBeNull();
        tt.ShouldBe(TransformType.Partition);
        idxs.ShouldBe([1, 2, 3]);
        intArg.ShouldBe(10);

        // {{wildcard(2)}}
        (tt, idxs, intArg, _, err) = SubjectTransform.IndexPlaceHolders("{{wildcard(2)}}");
        err.ShouldBeNull();
        tt.ShouldBe(TransformType.Wildcard);
        idxs.Length.ShouldBe(1);
        idxs[0].ShouldBe(2);
        intArg.ShouldBe(-1);

        // {{SplitFromLeft(2,1)}}
        int pos;
        (tt, idxs, pos, _, err) = SubjectTransform.IndexPlaceHolders("{{SplitFromLeft(2,1)}}");
        err.ShouldBeNull();
        tt.ShouldBe(TransformType.SplitFromLeft);
        idxs.Length.ShouldBe(1);
        idxs[0].ShouldBe(2);
        pos.ShouldBe(1);

        // {{SplitFromRight(3,2)}}
        (tt, idxs, pos, _, err) = SubjectTransform.IndexPlaceHolders("{{SplitFromRight(3,2)}}");
        err.ShouldBeNull();
        tt.ShouldBe(TransformType.SplitFromRight);
        idxs.Length.ShouldBe(1);
        idxs[0].ShouldBe(3);
        pos.ShouldBe(2);

        // {{SliceFromLeft(2,2)}}
        (tt, idxs, var sliceSize, _, err) = SubjectTransform.IndexPlaceHolders("{{SliceFromLeft(2,2)}}");
        err.ShouldBeNull();
        tt.ShouldBe(TransformType.SliceFromLeft);
        idxs.Length.ShouldBe(1);
        idxs[0].ShouldBe(2);
        sliceSize.ShouldBe(2);

        // {{Left(3,2)}}
        (tt, idxs, pos, _, err) = SubjectTransform.IndexPlaceHolders("{{Left(3,2)}}");
        err.ShouldBeNull();
        tt.ShouldBe(TransformType.Left);
        idxs.Length.ShouldBe(1);
        idxs[0].ShouldBe(3);
        pos.ShouldBe(2);

        // {{Right(3,2)}}
        (tt, idxs, pos, _, err) = SubjectTransform.IndexPlaceHolders("{{Right(3,2)}}");
        err.ShouldBeNull();
        tt.ShouldBe(TransformType.Right);
        idxs.Length.ShouldBe(1);
        idxs[0].ShouldBe(3);
        pos.ShouldBe(2);
    }

    [Fact]
    public void SubjectTransformHelpers_ShouldTokenizeAndUntokenize()
    {
        // Mirror: TestSubjectTransformHelpers

        // transformUntokenize — no placeholders
        var (filter, phs) = SubjectTransform.TransformUntokenize("bar");
        filter.ShouldBe("bar");
        phs.Length.ShouldBe(0);

        // transformUntokenize — dollar-sign placeholders
        (filter, phs) = SubjectTransform.TransformUntokenize("foo.$2.$1");
        filter.ShouldBe("foo.*.*");
        phs.ShouldBe(["$2", "$1"]);

        // transformUntokenize — mustache placeholders
        (filter, phs) = SubjectTransform.TransformUntokenize("foo.{{wildcard(2)}}.{{wildcard(1)}}");
        filter.ShouldBe("foo.*.*");
        phs.ShouldBe(["{{wildcard(2)}}", "{{wildcard(1)}}"]);

        // Strict reverse transform.
        var (tr, err) = SubjectTransform.NewStrict("foo.*.*", "bar.$2.{{Wildcard(1)}}");
        err.ShouldBeNull($"NewStrict failed: {err?.Message}");
        tr.ShouldNotBeNull();

        var subject = "foo.b.a";
        var transformed = tr!.TransformSubject(subject);
        var reverse = tr.Reverse();
        reverse.ShouldNotBeNull("reverse should not be null");
        reverse!.TransformSubject(transformed).ShouldBe(subject, "reverse of transform should return original subject");
    }

    [Fact]
    public void SubjectTransforms_ShouldValidateAndTransformCorrectly()
    {
        // Mirror: TestSubjectTransforms
        void ShouldErr(string src, string dest, bool strict)
        {
            var (t, e) = SubjectTransform.NewWithStrict(src, dest, strict);
            t.ShouldBeNull($"expected error but got transform for src={src}, dest={dest}");
            var isValid = ReferenceEquals(e, ServerErrors.ErrBadSubject) ||
                          (e is MappingDestinationException mde && mde.Is(ServerErrors.ErrInvalidMappingDestination));
            isValid.ShouldBeTrue(
                $"Expected ErrBadSubject or ErrInvalidMappingDestination for src={src}, dest={dest}, got: {e}");
        }

        ShouldErr("foo..", "bar", false);
        ShouldErr("foo.*", "bar.*", false);
        ShouldErr("foo.*", "bar.$2", false);
        ShouldErr("foo.*", "bar.$1.>", false);
        ShouldErr("foo.>", "bar.baz", false);
        ShouldErr("foo.*.*", "bar.$2", true);
        ShouldErr("foo.*", "foo.$foo", true);
        ShouldErr("foo.*", "bar.{{Partition(2,1)}}", true);
        ShouldErr("foo.*", "foo.{{wildcard(2)}}", false);
        ShouldErr("foo.*", "foo.{{unimplemented(1)}}", false);
        ShouldErr("foo.*", "foo.{{partition()}}", false);
        ShouldErr("foo.*", "foo.{{random()}}", false);
        ShouldErr("foo.*", "foo.{{wildcard(foo)}}", false);
        ShouldErr("foo.*", "foo.{{wildcard()}}", false);
        ShouldErr("foo.*", "foo.{{wildcard(1,2)}}", false);
        ShouldErr("foo.*", "foo.{{ wildcard5) }}", false);
        ShouldErr("foo.*", "foo.{{splitLeft(2,2}}", false);
        ShouldErr("foo", "bla.{{wildcard(1)}}", false);
        ShouldErr("foo.*", $"foo.{{{{partition({(long)int.MaxValue + 1})}}}}", false);
        ShouldErr("foo.*", $"foo.{{{{random({(long)int.MaxValue + 1})}}}}", false);

        SubjectTransform? ShouldBeOK(string src, string dest, bool strict)
        {
            var (tr, err) = SubjectTransform.NewWithStrict(src, dest, strict);
            err.ShouldBeNull($"Got error {err} for src={src}, dest={dest}");
            return tr;
        }

        ShouldBeOK("foo.*", "bar.{{Wildcard(1)}}", true);
        ShouldBeOK("foo.*.*", "bar.$2", false);
        ShouldBeOK("foo.*.*", "bar.{{wildcard(1)}}", false);
        ShouldBeOK("foo.*.*", "bar.{{partition(1)}}", false);
        ShouldBeOK("foo.*.*", "bar.{{random(5)}}", false);
        ShouldBeOK("foo", "bar", false);
        ShouldBeOK("foo.*.bar.*.baz", "req.$2.$1", false);
        ShouldBeOK("baz.>", "mybaz.>", false);
        ShouldBeOK("*", "{{splitfromleft(1,1)}}", false);
        ShouldBeOK("", "prefix.>", false);
        ShouldBeOK("*.*", "{{partition(10,1,2)}}", false);
        ShouldBeOK("foo.*.*", "foo.{{wildcard(1)}}.{{wildcard(2)}}.{{partition(5,1,2)}}", false);
        ShouldBeOK("foo.*", $"foo.{{{{partition({int.MaxValue})}}}}", false);
        ShouldBeOK("foo.*", $"foo.{{{{random({int.MaxValue})}}}}", false);
        ShouldBeOK("foo.bar", $"foo.{{{{random({int.MaxValue})}}}}", false);

        void ShouldMatch(string src, string dest, string sample, params string[] expected)
        {
            var tr = ShouldBeOK(src, dest, false);
            if (tr == null) return;
            var (s, err2) = tr.Match(sample);
            err2.ShouldBeNull($"Match error: {err2}");
            expected.ShouldContain(s, $"Transform {src}→{dest} on '{sample}', got '{s}'");
        }

        ShouldMatch("", "prefix.>", "foo", "prefix.foo");
        ShouldMatch("foo", "", "foo", "foo");
        ShouldMatch("foo", "bar", "foo", "bar");
        ShouldMatch("foo.*.bar.*.baz", "req.$2.$1", "foo.A.bar.B.baz", "req.B.A");
        ShouldMatch("foo.*.bar.*.baz", "req.{{wildcard(2)}}.{{wildcard(1)}}", "foo.A.bar.B.baz", "req.B.A");
        ShouldMatch("baz.>", "my.pre.>", "baz.1.2.3", "my.pre.1.2.3");
        ShouldMatch("baz.>", "foo.bar.>", "baz.1.2.3", "foo.bar.1.2.3");
        ShouldMatch("*", "foo.bar.$1", "foo", "foo.bar.foo");
        ShouldMatch("*", "{{splitfromleft(1,3)}}", "12345", "123.45");
        ShouldMatch("*", "{{SplitFromRight(1,3)}}", "12345", "12.345");
        ShouldMatch("*", "{{SliceFromLeft(1,3)}}", "1234567890", "123.456.789.0");
        ShouldMatch("*", "{{SliceFromRight(1,3)}}", "1234567890", "1.234.567.890");
        ShouldMatch("*", "{{split(1,-)}}", "-abc-def--ghi-", "abc.def.ghi");
        ShouldMatch("*", "{{split(1,-)}}", "abc-def--ghi-", "abc.def.ghi");
        ShouldMatch("*.*", "{{split(2,-)}}.{{splitfromleft(1,2)}}", "foo.-abc-def--ghij-", "abc.def.ghij.fo.o");
        ShouldMatch("*", "{{right(1,1)}}", "1234", "4");
        ShouldMatch("*", "{{right(1,3)}}", "1234", "234");
        ShouldMatch("*", "{{right(1,6)}}", "1234", "1234");
        ShouldMatch("*", "{{left(1,1)}}", "1234", "1");
        ShouldMatch("*", "{{left(1,3)}}", "1234", "123");
        ShouldMatch("*", "{{left(1,6)}}", "1234", "1234");
        ShouldMatch("*", "bar.{{partition(0)}}", "baz", "bar.0");
        ShouldMatch("*", "bar.{{partition(10, 0)}}", "foo", "bar.3");
        ShouldMatch("*.*", "bar.{{partition(10)}}", "foo.bar", "bar.6");
        ShouldMatch("*", "bar.{{partition(10)}}", "foo", "bar.3");
        ShouldMatch("*", "bar.{{partition(10)}}", "baz", "bar.0");
        ShouldMatch("*", "bar.{{partition(10)}}", "qux", "bar.9");
        ShouldMatch("*", "bar.{{random(0)}}", "qux", "bar.0");

        // random(6) — any value 0–5 is acceptable.
        for (var i = 0; i < 100; i++)
            ShouldMatch("*", "bar.{{random(6)}}", "qux",
                "bar.0", "bar.1", "bar.2", "bar.3", "bar.4", "bar.5");

        ShouldBeOK("foo.bar", "baz.{{partition(10)}}", false);
        ShouldMatch("foo.bar", "baz.{{partition(10)}}", "foo.bar", "baz.6");
        ShouldMatch("foo.baz", "qux.{{partition(10)}}", "foo.baz", "qux.4");
        ShouldMatch("test.subject", "result.{{partition(5)}}", "test.subject", "result.0");
    }

    [Fact]
    public void TransformTokenizedSubject_ShouldNotPanicOnMissingToken()
    {
        // Mirror: TestSubjectTransformDoesntPanicTransformingMissingToken
        var (tr, err) = SubjectTransform.New("foo.*", "one.two.{{wildcard(1)}}");
        err.ShouldBeNull();
        tr.ShouldNotBeNull();

        // Should not throw even when the token at index 1 is missing.
        var result = tr!.TransformTokenizedSubject(["foo"]);
        result.ShouldBe("one.two.");
    }
}
