using System;
using System.Collections.Generic;
using System.IO;
using eagle;
using Net.Eagle0.Eagle.Views;
using NUnit.Framework;

namespace eagle0.Tests {
    [TestFixture]
    public class DynamicTextBindingTests {
        private static class Placeholders {
            public static readonly DynamicTextPlaceholderName CompanionName =
                    new(nameof(CompanionName));
            public static readonly DynamicTextPlaceholderName HeroName = new(nameof(HeroName));
            public static readonly DynamicTextPlaceholderName UnusedName = new(nameof(UnusedName));
        }

        [Test]
        public void Constructor_ThrowsForNullTemplate() {
            Assert.Throws<ArgumentNullException>(
                    () => new DynamicTextBinding(null, new List<DynamicPlaceholder>()));
        }

        [Test]
        public void Constructor_ThrowsForNullPlaceholders() {
            Assert.Throws<ArgumentNullException>(
                    () => new DynamicTextBinding(
                            DynamicTextTemplate.Parsed("No placeholders."),
                            null));
        }

        [Test]
        public void Constructor_ThrowsForNullPlaceholderEntry() {
            var exception = Assert.Throws<ArgumentException>(
                    () => new DynamicTextBinding(
                            DynamicTextTemplate.Parsed("No placeholders."),
                            new List<DynamicPlaceholder> { null }));

            StringAssert.Contains(
                    "Dynamic text placeholders cannot contain null entries.",
                    exception.Message);
        }

        [Test]
        public void Constructor_ThrowsForDuplicatePlaceholders() {
            var exception = Assert.Throws<ArgumentException>(
                    () => new DynamicTextBinding(
                            DynamicTextTemplate.Parsed("{HeroName}"),
                            new List<DynamicPlaceholder> {
                                DynamicPlaceholder.StaticText(Placeholders.HeroName, "Xiahou Dun"),
                                DynamicPlaceholder.StaticText(Placeholders.HeroName, "Xiahou Yuan"),
                            }));

            StringAssert.Contains(
                    "Duplicate dynamic text placeholders: HeroName",
                    exception.Message);
        }

        [Test]
        public void Constructor_ThrowsForMissingPlaceholders() {
            var exception = Assert.Throws<ArgumentException>(
                    () => new DynamicTextBinding(
                            DynamicTextTemplate.Parsed("{HeroName} challenged {RivalName}."),
                            new List<DynamicPlaceholder> {
                                DynamicPlaceholder.StaticText(Placeholders.HeroName, "Guan Yu"),
                            }));

            StringAssert.Contains(
                    "Dynamic text template placeholders missing values: RivalName",
                    exception.Message);
        }

        [Test]
        public void Constructor_ReportsMissingPlaceholdersInTemplateOrder() {
            var exception = Assert.Throws<ArgumentException>(
                    () => new DynamicTextBinding(
                            DynamicTextTemplate.Parsed("{CompanionName} challenged {HeroName}."),
                            new List<DynamicPlaceholder>()));

            StringAssert.Contains(
                    "Dynamic text template placeholders missing values: CompanionName, HeroName",
                    exception.Message);
        }

        [Test]
        public void Constructor_ThrowsForUnusedPlaceholders() {
            var exception = Assert.Throws<ArgumentException>(
                    () => new DynamicTextBinding(
                            DynamicTextTemplate.Parsed("{HeroName} arrived."),
                            new List<DynamicPlaceholder> {
                                DynamicPlaceholder.StaticText(Placeholders.HeroName, "Zhao Yun"),
                                DynamicPlaceholder.StaticText(Placeholders.UnusedName, "Ma Chao"),
                            }));

            StringAssert.Contains(
                    "Dynamic text placeholders not used by template: UnusedName",
                    exception.Message);
        }

        [Test]
        public void Constructor_AllowsLiteralSuffixWithBraces() {
            var binding = new DynamicTextBinding(
                    DynamicTextTemplate.Parsed("{HeroName} departed")
                            .WithLiteralSuffix(" after hearing {narrative text with braces}."),
                    new List<DynamicPlaceholder> {
                        DynamicPlaceholder.StaticText(Placeholders.HeroName, "Sun Shangxiang"),
                    });

            Assert.AreEqual(
                    "Sun Shangxiang departed after hearing {narrative text with braces}.",
                    binding.Text);
        }

        [Test]
        public void Constructor_AllowsParsedSuffixWithPlaceholders() {
            var binding = new DynamicTextBinding(
                    DynamicTextTemplate.Parsed("{HeroName} departed")
                            .WithParsedSuffix(" with {CompanionName}."),
                    new List<DynamicPlaceholder> {
                        DynamicPlaceholder.StaticText(Placeholders.HeroName, "Sun Shangxiang"),
                        DynamicPlaceholder.StaticText(Placeholders.CompanionName, "Liu Bei"),
                    });

            Assert.AreEqual("Sun Shangxiang departed with Liu Bei.", binding.Text);
        }

        [Test]
        public void Parsed_ThrowsForNullText() {
            Assert.Throws<ArgumentNullException>(() => DynamicTextTemplate.Parsed(null));
        }

        [Test]
        public void WithParsedSuffix_ThrowsForNullSuffix() {
            Assert.Throws<ArgumentNullException>(
                    () => DynamicTextTemplate.Parsed("{HeroName}").WithParsedSuffix(null));
        }

        [Test]
        public void Constructor_AllowsRepeatedTemplatePlaceholder() {
            var binding = new DynamicTextBinding(
                    DynamicTextTemplate.Parsed("{HeroName} honored {HeroName}."),
                    new List<DynamicPlaceholder> {
                        DynamicPlaceholder.StaticText(Placeholders.HeroName, "Zhao Yun"),
                    });

            Assert.AreEqual("Zhao Yun honored Zhao Yun.", binding.Text);
        }

        [Test]
        public void Constructor_ThrowsForNullStaticPlaceholderText() {
            Assert.Throws<ArgumentNullException>(
                    () => DynamicPlaceholder.StaticText(Placeholders.HeroName, null));
        }

        [Test]
        public void Constructor_ThrowsForNullGeneratedTextFallback() {
            Assert.Throws<ArgumentNullException>(
                    () => DynamicPlaceholder.HeroName(Placeholders.HeroName, new HeroView(), null));
        }

        [Test]
        public void Constructor_DoesNotRenderPlaceholderTokensFromPlaceholderValues() {
            var binding = new DynamicTextBinding(
                    DynamicTextTemplate.Parsed("{HeroName} challenged {CompanionName}."),
                    new List<DynamicPlaceholder> {
                        DynamicPlaceholder.StaticText(Placeholders.HeroName, "{CompanionName}"),
                        DynamicPlaceholder.StaticText(Placeholders.CompanionName, "Liu Bei"),
                    });

            Assert.AreEqual("{CompanionName} challenged Liu Bei.", binding.Text);
        }

        [Test]
        public void Render_PreservesMissingPlaceholderTokens() {
            var text = DynamicTextTemplate.Parsed("{HeroName} challenged {CompanionName}.")
                               .Render(new Dictionary<DynamicTextPlaceholderName, string> {
                                   { Placeholders.HeroName, "Guan Yu" },
                               });

            Assert.AreEqual("Guan Yu challenged {CompanionName}.", text);
        }

        [Test]
        public void Render_ThrowsForNullPlaceholderValues() {
            Assert.Throws<ArgumentNullException>(
                    () => DynamicTextTemplate.Parsed("{HeroName}").Render(null));
        }

        [Test]
        public void Render_ThrowsForNullPlaceholderValueEntries() {
            Assert.Throws<ArgumentException>(
                    () => DynamicTextTemplate.Parsed("{HeroName}")
                                  .Render(new Dictionary<DynamicTextPlaceholderName, string> {
                                      { Placeholders.HeroName, null },
                                  }));
        }

        [Test]
        public void Placeholder_ReturnsParsedPlaceholderToken() {
            Assert.AreEqual("{HeroName}", DynamicTextTemplate.Placeholder(Placeholders.HeroName));
        }

        [Test]
        public void TryParsePlaceholderToken_ReturnsPlaceholderName() {
            Assert.IsTrue(DynamicTextTemplate.TryParsePlaceholderToken("{HeroName}", out var name));

            Assert.AreEqual(Placeholders.HeroName, name);
        }

        [Test]
        public void TryParsePlaceholderToken_RejectsNestedPlaceholderToken() {
            Assert.IsFalse(DynamicTextTemplate.TryParsePlaceholderToken("{{HeroName}}", out _));
        }

        [Test]
        public void PlaceholderName_WithNumericSuffix_AppendsSuffix() {
            Assert.AreEqual("HeroName2", Placeholders.HeroName.WithNumericSuffix(2).Value);
        }

        [Test]
        public void PlaceholderName_ThrowsForInvalidNames() {
            Assert.Throws<ArgumentException>(() => new DynamicTextPlaceholderName(""));
            Assert.Throws<ArgumentException>(() => new DynamicTextPlaceholderName("{HeroName}"));
            Assert.Throws<ArgumentException>(() => new DynamicTextPlaceholderName("Hero.Name"));
            Assert.Throws<ArgumentException>(() => new DynamicTextPlaceholderName("1HeroName"));
        }

        [Test]
        public void PlaceholderName_AllowsUnicodeLetters() {
            var placeholderName = new DynamicTextPlaceholderName("\u82f1\u96c4Name");
            var binding = new DynamicTextBinding(
                    DynamicTextTemplate.Parsed("{\u82f1\u96c4Name} arrived."),
                    new List<DynamicPlaceholder> {
                        DynamicPlaceholder.StaticText(placeholderName, "Zhao Yun"),
                    });

            Assert.AreEqual("Zhao Yun arrived.", binding.Text);
        }

        [Test]
        public void Notifications_DoNotParseTemplatesInlineAtCallBoundary() {
            var offenders = new List<string>();

            foreach (var sourceLine in NotificationSourceLines()) {
                if (!sourceLine.Text.Contains("textTemplate: DynamicTextTemplate.Parsed(")) {
                    continue;
                }

                offenders.Add(sourceLine.Location);
            }

            Assert.IsEmpty(
                    offenders,
                    "Use a typed DynamicTextTemplate local before passing textTemplate: " +
                            string.Join(", ", offenders));
        }

        [Test]
        public void Notifications_DoNotUseRawParsedPlaceholderTokens() {
            var offenders = new List<string>();

            foreach (var sourceLine in NotificationSourceLines()) {
                if (!ContainsRawParsedPlaceholderToken(sourceLine.Text)) { continue; }

                offenders.Add(sourceLine.Location);
            }

            Assert.IsEmpty(
                    offenders,
                    "Use DynamicTextTemplate.Placeholder(...) instead of raw {Placeholder} tokens: " +
                            string.Join(", ", offenders));
        }

        [Test]
        public void RawPlaceholderAudit_FindsRawPlaceholderInPlainString() {
            Assert.IsTrue(ContainsRawParsedPlaceholderToken(
                    "var textTemplate = DynamicTextTemplate.Parsed(\"{HeroName} arrived.\");"));
        }

        [Test]
        public void RawPlaceholderAudit_IgnoresCSharpInterpolation() {
            Assert.IsFalse(ContainsRawParsedPlaceholderToken(
                    "var textTemplate = DynamicTextTemplate.Parsed($\"{heroName} arrived.\");"));
        }

        [Test]
        public void RawPlaceholderAudit_FindsEscapedPlaceholderInInterpolatedString() {
            Assert.IsTrue(ContainsRawParsedPlaceholderToken(
                    "var textTemplate = DynamicTextTemplate.Parsed($\"{{HeroName}} arrived.\");"));
        }

        [Test]
        public void RawPlaceholderAudit_IgnoresNestedPlainStringBraces() {
            Assert.IsFalse(ContainsRawParsedPlaceholderToken(
                    "var textTemplate = DynamicTextTemplate.Parsed(\"{{HeroName}} arrived.\");"));
        }

        private static bool ContainsRawParsedPlaceholderToken(string line) {
            for (int i = 0; i < line.Length; i++) {
                if (line[i] != '"') { continue; }

                var interpolated = i > 0 && line[i - 1] == '$';
                var endQuoteIndex = FindStringEnd(line, i + 1);
                if (endQuoteIndex < 0) { return false; }

                var content = line.Substring(i + 1, endQuoteIndex - i - 1);
                if (ContainsRawParsedPlaceholderToken(content, interpolated)) { return true; }

                i = endQuoteIndex;
            }

            return false;
        }

        private static int FindStringEnd(string line, int startIndex) {
            for (int i = startIndex; i < line.Length; i++) {
                if (line[i] == '\\') {
                    i++;
                    continue;
                }

                if (line[i] == '"') { return i; }
            }

            return -1;
        }

        private static bool ContainsRawParsedPlaceholderToken(
                string stringLiteralContent,
                bool interpolated) {
            for (int i = 0; i < stringLiteralContent.Length; i++) {
                if (stringLiteralContent[i] != '{') { continue; }

                if (interpolated) {
                    if (i + 1 >= stringLiteralContent.Length ||
                        stringLiteralContent[i + 1] != '{') {
                        continue;
                    }

                    var closeBraceIndex =
                            stringLiteralContent.IndexOf("}}", i + 2, StringComparison.Ordinal);
                    if (closeBraceIndex < 0) { return false; }

                    var escapedToken =
                            "{" + stringLiteralContent.Substring(i + 2, closeBraceIndex - i - 2) +
                            "}";
                    if (DynamicTextTemplate.TryParsePlaceholderToken(escapedToken, out _)) {
                        return true;
                    }

                    i = closeBraceIndex + 1;
                } else {
                    var closeBraceIndex = stringLiteralContent.IndexOf('}', i + 1);
                    if (closeBraceIndex < 0) { return false; }

                    var token = stringLiteralContent.Substring(i, closeBraceIndex - i + 1);
                    if (DynamicTextTemplate.TryParsePlaceholderToken(token, out _)) { return true; }

                    i = closeBraceIndex;
                }
            }

            return false;
        }

        [Test]
        public void Notifications_DoNotDeclarePlaceholderNamesAsStrings() {
            var offenders = new List<string>();

            foreach (var sourceLine in NotificationSourceLines()) {
                if (!LooksLikePublicStringDeclaration(sourceLine.Text)) { continue; }

                offenders.Add(sourceLine.Location);
            }

            Assert.IsEmpty(
                    offenders,
                    "Declare notification placeholders as DynamicTextPlaceholderName, not string: " +
                            string.Join(", ", offenders));
        }

        private static bool LooksLikePublicStringDeclaration(string line) {
            return line.Contains("public ") &&
                   (line.Contains("const string ") || line.Contains("static readonly string ")) &&
                   line.Contains("=");
        }

        private static IEnumerable<SourceLine> NotificationSourceLines() {
            var notificationsPath =
                    Path.Combine(UnityEngine.Application.dataPath, "Eagle", "Notifications");

            foreach (var path in Directory
                             .GetFiles(notificationsPath, "*.cs", SearchOption.AllDirectories)) {
                var lineNumber = 0;
                foreach (var line in File.ReadLines(path)) {
                    lineNumber++;
                    yield return new SourceLine($"{Path.GetFileName(path)}:{lineNumber}", line);
                }
            }
        }

        private class SourceLine {
            public string Location { get; }
            public string Text { get; }

            public SourceLine(string location, string text) {
                Location = location;
                Text = text;
            }
        }
    }
}
