from mars_bot.format import md_to_html, md_to_html_chunks, MAX_HTML_LENGTH, TARGET_MD_LENGTH


def test_escape_amp():
    assert md_to_html("a & b") == "a &amp; b"


def test_escape_lt_gt():
    assert md_to_html("a < b > c") == "a &lt; b &gt; c"


def test_plain_text_unchanged():
    assert md_to_html("просто текст") == "просто текст"


def test_bold():
    assert md_to_html("**жирно**") == "<b>жирно</b>"


def test_italic():
    assert md_to_html("*курсив*") == "<i>курсив</i>"


def test_bold_and_italic_together():
    assert md_to_html("**bold** и *italic*") == "<b>bold</b> и <i>italic</i>"


def test_heading_h1():
    assert md_to_html("# Заголовок") == "<b>Заголовок</b>"


def test_heading_h2():
    assert md_to_html("## Заголовок") == "<b>Заголовок</b>"


def test_heading_h3():
    assert md_to_html("### Заголовок") == "<b>Заголовок</b>"


def test_link():
    assert md_to_html("[ссылка](https://example.com)") == '<a href="https://example.com">ссылка</a>'


def test_link_with_query_params():
    assert md_to_html("[x](https://a.com/?a=1&b=2)") == '<a href="https://a.com/?a=1&amp;b=2">x</a>'


def test_link_text_escaped():
    assert md_to_html("[<x>](https://a.com)") == '<a href="https://a.com">&lt;x&gt;</a>'


def test_dash_list_becomes_bullet():
    # NB: спека первой версии говорила «оставляем дефис как есть». Здесь сознательно
    # отходим — `•` читабельнее в TG, дефис в начале строки легко спутать с минусом.
    # Дизайн обновлён под это поведение.
    assert md_to_html("- first\n- second") == "• first\n• second"


def test_asterisk_list_becomes_bullet():
    assert md_to_html("* first\n* second") == "• first\n• second"


def test_escape_inside_bold():
    assert md_to_html("**a & b**") == "<b>a &amp; b</b>"


def test_asterisk_inside_word_not_italic():
    assert md_to_html("a*b*c") == "a*b*c"


def test_unclosed_bold_left_as_text():
    assert md_to_html("text with ** unclosed") == "text with ** unclosed"


def test_combined():
    md = "## Главные боли\n\n- **Никто** не понимал, кто [за что](https://x.com) отвечает."
    expected = (
        "<b>Главные боли</b>\n"
        "\n"
        "• <b>Никто</b> не понимал, кто <a href=\"https://x.com\">за что</a> отвечает."
    )
    assert md_to_html(md) == expected


def test_nested_italic_around_bold_no_nul_leak():
    # Critical regression test: *a **b** c* must not leak \x00TAG placeholders.
    # Italic-around-bold is a plausible pattern in saммари ("*особенно **этот** пункт*").
    result = md_to_html("*a **b** c*")
    assert "\x00" not in result, f"NUL byte leaked into output: {result!r}"
    assert "TAG" not in result, f"placeholder key leaked into output: {result!r}"
    # Expected: italic wraps the whole, bold wraps the inner word.
    assert result == "<i>a <b>b</b> c</i>"


def test_nested_bold_around_italic_no_nul_leak():
    # Mirror case: **a *b* c** — bold pass matches **a *b* c**, italic might match *b*
    # Either way, no NUL/TAG must leak.
    result = md_to_html("**a *b* c**")
    assert "\x00" not in result
    assert "TAG" not in result


def test_user_input_with_nul_byte_is_stripped():
    # Defensive: a malicious or accidental \x00 in input must not produce
    # broken HTML or collide with our placeholder scheme.
    result = md_to_html("\x00 hello \x00")
    assert "\x00" not in result
    assert "hello" in result


def test_heading_with_trailing_hashes_kept_literal():
    # ATX-style closing '#' is not stripped. Pinning current behaviour.
    assert md_to_html("## Hi ##") == "<b>Hi ##</b>"


def test_chunks_empty_string_returns_empty_list():
    assert md_to_html_chunks("") == []


def test_chunks_whitespace_only_returns_empty_list():
    assert md_to_html_chunks("   \n\n  \t  ") == []


def test_chunks_single_char_returns_one_chunk():
    # Boundary sanity: minimum non-empty input stays as one chunk.
    assert md_to_html_chunks("x") == ["x"]


def test_chunks_short_text_single_chunk():
    chunks = md_to_html_chunks("короткое сообщение")
    assert chunks == ["короткое сообщение"]


def test_chunks_short_with_formatting():
    chunks = md_to_html_chunks("**bold** text")
    assert chunks == ["<b>bold</b> text"]


def test_chunks_split_by_paragraphs():
    para = "a" * 1500
    md = "\n\n".join([para, para, para])  # ~4500 chars markdown
    chunks = md_to_html_chunks(md)
    assert len(chunks) >= 2
    for chunk in chunks:
        assert len(chunk) <= MAX_HTML_LENGTH


def test_chunks_preserve_paragraphs_on_boundary():
    # Каждый абзац должен оказаться целиком в одном чанке (никаких '\n\n' внутри одного абзаца поделено)
    p1 = "abc " * 200  # ~800
    p2 = "def " * 200
    p3 = "ghi " * 200
    p4 = "jkl " * 200
    p5 = "mno " * 200
    md = "\n\n".join([p1, p2, p3, p4, p5])  # ~4000 markdown, > TARGET_MD_LENGTH=3500
    chunks = md_to_html_chunks(md)
    # Каждый кусок не содержит частичных абзацев — проверяем что abc, def итд не разорваны
    joined = "".join(chunks)
    for marker in ["abc", "def", "ghi", "jkl", "mno"]:
        assert marker in joined


def test_chunks_escape_explosion_still_fits():
    # Текст почти из одних амперсандов: после escape & → &amp; длина растёт в 5 раз.
    # Первый проход даст HTML > MAX_HTML_LENGTH — должна сработать рекурсия с меньшим md-target.
    para = "&" * 3400
    chunks = md_to_html_chunks(para)
    for chunk in chunks:
        assert len(chunk) <= MAX_HTML_LENGTH, f"chunk too long: {len(chunk)}"


def test_chunks_very_long_paragraph_sentence_split():
    sentences = [f"Sentence number {i} with extra padding text. " for i in range(200)]
    md = "".join(sentences)
    chunks = md_to_html_chunks(md)
    for chunk in chunks:
        assert len(chunk) <= MAX_HTML_LENGTH


def test_chunks_never_split_link_tag():
    # Длинная ссылка должна целиком оказаться в одном чанке — никогда не быть разорванной
    long_intro = "intro paragraph. " * 200
    link_md = "[click here](https://example.com/very/long/path?a=1&b=2&c=3&d=4)"
    long_outro = "outro paragraph. " * 200
    md = f"{long_intro}\n\n{link_md}\n\n{long_outro}"
    chunks = md_to_html_chunks(md)
    expected_a_tag = '<a href="https://example.com/very/long/path?a=1&amp;b=2&amp;c=3&amp;d=4">click here</a>'
    matches = [c for c in chunks if expected_a_tag in c]
    assert len(matches) == 1, f"link tag must appear intact in exactly one chunk, got chunks={[c[:80] for c in chunks]}"
    # Никакой чанк не содержит фрагмента <a без закрывающего </a>
    for c in chunks:
        a_opens = c.count("<a ")
        a_closes = c.count("</a>")
        assert a_opens == a_closes, f"unbalanced <a>/</a> in chunk: {c[:200]}"


def test_chunks_never_split_amp_entity():
    # Длинный текст с & в середине — &amp; должен попасть в один чанк целиком
    pre = "abc " * 800
    post = " def" * 800
    md = pre + "X&Y" + post
    chunks = md_to_html_chunks(md)
    joined = "".join(chunks)
    assert "X&amp;Y" in joined
    # Никакой чанк не оканчивается на «висящий» фрагмент сущности
    for c in chunks:
        assert not c.endswith("&"), f"chunk ends with bare &: {c[-30:]!r}"
        assert not c.endswith("&a"), f"chunk ends with partial entity: {c[-30:]!r}"
        assert not c.endswith("&am"), f"chunk ends with partial entity: {c[-30:]!r}"
        assert not c.endswith("&amp"), f"chunk ends with partial entity: {c[-30:]!r}"


def test_chunks_never_split_bold():
    # Длинный текст с bold-фрагментом в середине — <b>...</b> должен попасть в один чанк
    pre = "intro. " * 500
    post = " outro." * 500
    md = pre + "\n\n**important bold phrase**\n\n" + post
    chunks = md_to_html_chunks(md)
    matches = [c for c in chunks if "<b>important bold phrase</b>" in c]
    assert len(matches) == 1
    for c in chunks:
        b_opens = c.count("<b>")
        b_closes = c.count("</b>")
        assert b_opens == b_closes, f"unbalanced <b>/</b>: {c[:200]}"


def test_split_by_words_does_not_lose_long_word_data():
    # Прямая проверка _split_by_words: безумно длинное слово не должно теряться
    from mars_bot.format import _split_by_words
    word = "x" * 250
    chunks = _split_by_words(word, max_len=100)
    assert "".join(chunks) == word  # вся буква-в-букву сохранена
    assert all(len(c) <= 100 for c in chunks)
