package md2html;

import base.*;

import java.util.*;

/**
 * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
 */
public class Md2HtmlTester {
    private static final Map<String, String> ESCAPES = Map.of("<", "&lt;", ">", "&gt;");

    private final Map<String, Generator> elements = new HashMap<>();
    private final Map<String, List<String>> tags = new LinkedHashMap<>();
    private final List<Pair<String, String>> tests = new ArrayList<>();

    public Md2HtmlTester() {
        addElement("em", "*");
        addElement("em", "_");
        addElement("strong", "**");
        addElement("strong", "__");
        addElement("s", "--");
        addElement("code", "`");

        test(
                "# Заголовок первого уровня\n\n",
                "<h1>Заголовок первого уровня</h1>"
        );
        test(
                "## Второго\n\n",
                "<h2>Второго</h2>"
        );
        test(
                "### Третьего ## уровня\n\n",
                "<h3>Третьего ## уровня</h3>"
        );
        test(
                "#### Четвертого\n# Все еще четвертого\n\n",
                "<h4>Четвертого\n# Все еще четвертого</h4>"
        );
        test(
                "Этот абзац текста\nсодержит две строки.",
                "<p>Этот абзац текста\nсодержит две строки.</p>"
        );
        test(
                "    # Может показаться, что это заголовок.\nНо нет, это абзац, начинающийся с `#`.\n\n",
                "<p>    # Может показаться, что это заголовок.\nНо нет, это абзац, начинающийся с <code>#</code>.</p>"
        );
        test(
                "#И это не заголовок.\n\n",
                "<p>#И это не заголовок.</p>"
        );
        test(
                "###### Заголовки могут быть многострочными\n(и с пропуском заголовков предыдущих уровней)\n\n",
                "<h6>Заголовки могут быть многострочными\n(и с пропуском заголовков предыдущих уровней)</h6>"
        );
        test(
                "Мы все любим *выделять* текст _разными_ способами.\n**Сильное выделение**, используется гораздо реже,\nно __почему бы и нет__?\nНемного --зачеркивания-- еще никому не вредило.\nКод представляется элементом `code`.\n\n",
                "<p>Мы все любим <em>выделять</em> текст <em>разными</em> способами.\n<strong>Сильное выделение</strong>, используется гораздо реже,\nно <strong>почему бы и нет</strong>?\nНемного <s>зачеркивания</s> еще никому не вредило.\nКод представляется элементом <code>code</code>.</p>"
        );
        test(
                "Обратите внимание, как экранируются специальные\nHTML-символы, такие как `<`, `>` и `&`.\n\n",
                "<p>Обратите внимание, как экранируются специальные\nHTML-символы, такие как <code>&lt;</code>, <code>&gt;</code> и <code>&amp;</code>.</p>"
        );
        test(
                "Экранирование должно работать во всех местах: <>&.\n\n",
                "<p>Экранирование должно работать во всех местах: &lt;&gt;&amp;.</p>"
        );
        test(
                "Знаете ли вы, что в Markdown, одиночные * и _\nне означают выделение?\nОни так же могут быть заэкранированы\nпри помощи обратного слэша: \\*.",
                "<p>Знаете ли вы, что в Markdown, одиночные * и _\nне означают выделение?\nОни так же могут быть заэкранированы\nпри помощи обратного слэша: *.</p>"
        );
        test(
                "\n\n\nЛишние пустые строки должны игнорироваться.\n\n\n\n",
                "<p>Лишние пустые строки должны игнорироваться.</p>"
        );
        test(
                "Любите ли вы *вложенные __выделения__* так,\nкак __--люблю--__ их я?",
                "<p>Любите ли вы <em>вложенные <strong>выделения</strong></em> так,\nкак <strong><s>люблю</s></strong> их я?</p>"
        );

        test(
                """
                # Заголовок первого уровня

                ## Второго

                ### Третьего ## уровня

                #### Четвертого
                # Все еще четвертого

                Этот абзац текста
                содержит две строки.

                    # Может показаться, что это заголовок.
                Но нет, это абзац, начинающийся с `#`.

                #И это не заголовок.

                ###### Заголовки могут быть многострочными
                (и с пропуском заголовков предыдущих уровней)

                Мы все любим *выделять* текст _разными_ способами.
                **Сильное выделение**, используется гораздо реже,
                но __почему бы и нет__?
                Немного --зачеркивания-- еще никому не вредило.
                Код представляется элементом `code`.

                Обратите внимание, как экранируются специальные
                HTML-символы, такие как `<`, `>` и `&`.

                Знаете ли вы, что в Markdown, одиночные * и _
                не означают выделение?
                Они так же могут быть заэкранированы
                при помощи обратного слэша: \\*.



                Лишние пустые строки должны игнорироваться.

                Любите ли вы *вложенные __выделения__* так,
                как __--люблю--__ их я?
                """,
            """
                <h1>Заголовок первого уровня</h1>
                <h2>Второго</h2>
                <h3>Третьего ## уровня</h3>
                <h4>Четвертого
                # Все еще четвертого</h4>
                <p>Этот абзац текста
                содержит две строки.</p>
                <p>    # Может показаться, что это заголовок.
                Но нет, это абзац, начинающийся с <code>#</code>.</p>
                <p>#И это не заголовок.</p>
                <h6>Заголовки могут быть многострочными
                (и с пропуском заголовков предыдущих уровней)</h6>
                <p>Мы все любим <em>выделять</em> текст <em>разными</em> способами.
                <strong>Сильное выделение</strong>, используется гораздо реже,
                но <strong>почему бы и нет</strong>?
                Немного <s>зачеркивания</s> еще никому не вредило.
                Код представляется элементом <code>code</code>.</p>
                <p>Обратите внимание, как экранируются специальные
                HTML-символы, такие как <code>&lt;</code>, <code>&gt;</code> и <code>&amp;</code>.</p>
                <p>Знаете ли вы, что в Markdown, одиночные * и _
                не означают выделение?
                Они так же могут быть заэкранированы
                при помощи обратного слэша: *.</p>
                <p>Лишние пустые строки должны игнорироваться.</p>
                <p>Любите ли вы <em>вложенные <strong>выделения</strong></em> так,
                как <strong><s>люблю</s></strong> их я?</p>
                """
        );

        test("# Без перевода строки в конце", "<h1>Без перевода строки в конце</h1>");
        test("# Один перевод строки в конце\n", "<h1>Один перевод строки в конце</h1>");
        test("# Два перевода строки в конце\n\n", "<h1>Два перевода строки в конце</h1>");
        test(
                "Выделение может *начинаться на одной строке,\n а заканчиваться* на другой",
                "<p>Выделение может <em>начинаться на одной строке,\n а заканчиваться</em> на другой</p>"
        );
        test("# *Выделение* и `код` в заголовках", "<h1><em>Выделение</em> и <code>код</code> в заголовках</h1>");
    }

    protected void addElement(final String tag, final String markup) {
        addElement(tag, markup, markup);
    }

    protected void addElement(final String tag, final String begin, final String end) {
        addElement(tag, begin, (checker, markup, input, output) -> {
            checker.space(input, output);
            input.append(begin);
            open(output, tag);

            checker.word(input, output);
            checker.generate(markup, input, output);
            checker.word(input, output);

            input.append(end);
            close(output, tag);
            checker.space(input, output);
        });
    }

    public void addElement(final String tag, final String begin, final Generator generator) {
        Asserts.assertTrue("Duplicate element " + begin, elements.put(begin, generator) == null);
        tags.computeIfAbsent(tag, k -> new ArrayList<>()).add(begin);
    }

    private final Runner runner = Runner.packages("md2html").files("Md2Html");

    protected Md2HtmlTester test(final String input, final String output) {
        tests.add(Pair.of(input, output));
        return this;
    }

    protected Md2HtmlTester spoiled(final String input, final String output, final String... spoilers) {
        for (final String spoiler : spoilers) {
            final Indexer in = new Indexer(input, spoiler);
            final Indexer out = new Indexer(output, ESCAPES.getOrDefault(spoiler, spoiler));
            while (in.next() && out.next()) {
                tests.add(Pair.of(in.cut(), out.cut()));
                tests.add(Pair.of(in.escape(), output));
            }
        }
        return this;
    }

    private static class Indexer {
        private final String string;
        private final String spoiler;
        private int index = - 1;

        public Indexer(final String string, final String spoiler) {
            this.string = string;
            this.spoiler = spoiler;
        }

        public boolean next() {
            index = string.indexOf(spoiler, index + 1);
            return index >= 0;
        }

        public String cut() {
            return string.substring(0, index) + string.substring(index + spoiler.length());
        }

        public String escape() {
            return string.substring(0, index) + "\\" + string.substring(index);
        }
    }

    private static void open(final StringBuilder output, final String tag) {
        output.append("<").append(tag).append(">");
    }

    private static void close(final StringBuilder output, final String tag) {
        output.append("</").append(tag).append(">");
    }

    public void test(final TestCounter counter) {
        counter.scope("Testing " + String.join(", ", tags.keySet()), () -> new Checker(counter).test());
    }

    public class Checker extends BaseChecker {
        public Checker(final TestCounter counter) {
            super(counter);
        }

        protected void test() {
            for (final Pair<String, String> test : tests) {
                test(test);
            }

            for (final String markup : elements.keySet()) {
                randomTest(3, 10, List.of(markup));
            }

            final int d = TestCounter.DENOMINATOR;
            for (int i = 0; i < 10; i++) {
                randomTest(100, 1000, randomMarkup());
            }
            randomTest(100, 100_000 / d, randomMarkup());
        }

        private void test(final Pair<String, String> test) {
            runner.testEquals(counter, Arrays.asList(test.first().split("\n")), Arrays.asList(test.second().split("\n")));
        }

        private List<String> randomMarkup() {
            return Functional.map(tags.values(), random()::randomItem);
        }

        private void randomTest(final int paragraphs, final int length, final List<String> markup) {
            final StringBuilder input = new StringBuilder();
            final StringBuilder output = new StringBuilder();
            emptyLines(input);
            final List<String> markupList = new ArrayList<>(markup);
            for (int i = 0; i < paragraphs; i++) {
                final StringBuilder inputSB = new StringBuilder();
                paragraph(length, inputSB, output, markupList);
                input.append(inputSB);
                emptyLines(input);
            }
            test(Pair.of(input.toString(), output.toString()));
        }

        private void paragraph(final int length, final StringBuilder input, final StringBuilder output, final List<String> markup) {
            final int h = random().nextInt(0, 6);
            final String tag = h == 0 ? "p" : "h" + h;
            if (h > 0) {
                input.append(new String(new char[h]).replace('\0', '#')).append(" ");
            }

            open(output, tag);
            while (input.length() < length) {
                generate(markup, input, output);
                final String middle = random().randomString(ExtendedRandom.ENGLISH);
                input.append(middle).append("\n");
                output.append(middle).append("\n");
            }
            output.setLength(output.length() - 1);
            close(output, tag);

            output.append("\n");
            input.append("\n");
        }

        private void space(final StringBuilder input, final StringBuilder output) {
            if (random().nextBoolean()) {
                final String space = random().nextBoolean() ? " " : "\n";
                input.append(space);
                output.append(space);
            }
        }

        public void generate(final List<String> markup, final StringBuilder input, final StringBuilder output) {
            word(input, output);
            if (markup.isEmpty()) {
                return;
            }
            final String type = random().randomItem(markup);

            markup.remove(type);
            elements.get(type).generate(this, markup, input, output);
            markup.add(type);
        }

        protected void word(final StringBuilder input, final StringBuilder output) {
            final String word = random().randomString(random().randomItem(ExtendedRandom.ENGLISH, ExtendedRandom.GREEK, ExtendedRandom.RUSSIAN));
            input.append(word);
            output.append(word);
        }

        private void emptyLines(final StringBuilder sb) {
            while (random().nextBoolean()) {
                sb.append('\n');
            }
        }

        String generateInput(final List<String> markup) {
            final StringBuilder sb = new StringBuilder();
            generate(markup, sb, new StringBuilder());
            return sb.toString()
                    .replace("<", "")
                    .replace(">", "")
                    .replace("]", "");
        }
    }

    @FunctionalInterface
    public interface Generator {
        void generate(Checker checker, List<String> markup, StringBuilder input, StringBuilder output);
    }
}
