"""
Comprehensive tests for mdmin Python package.

Run with:  pytest python/tests/  (from project root)
       or: pytest               (from python/ directory)
"""

from __future__ import annotations

import textwrap

import pytest

from mdmin import compress, estimate_tokens, CompressResult, CompressionStats


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def c(text: str, level: str = "medium") -> str:
    """Compress and return just the output string."""
    return compress(text, level=level).output  # type: ignore[arg-type]


def stats(text: str, level: str = "medium") -> CompressionStats:
    return compress(text, level=level).stats  # type: ignore[arg-type]


# ---------------------------------------------------------------------------
# 1. Return types
# ---------------------------------------------------------------------------

class TestReturnTypes:
    def test_returns_compress_result(self):
        r = compress("Hello world.")
        assert isinstance(r, CompressResult)

    def test_stats_type(self):
        r = compress("Hello world.")
        assert isinstance(r.stats, CompressionStats)

    def test_stats_fields(self):
        s = stats("Hello world.")
        assert isinstance(s.input_tokens, int)
        assert isinstance(s.output_tokens, int)
        assert isinstance(s.saved, int)
        assert isinstance(s.pct, float)
        assert isinstance(s.input_chars, int)
        assert isinstance(s.output_chars, int)
        assert isinstance(s.level, str)

    def test_output_is_string(self):
        assert isinstance(compress("Hello.").output, str)


# ---------------------------------------------------------------------------
# 2. Edge cases — empty / trivial input
# ---------------------------------------------------------------------------

class TestEdgeCases:
    def test_empty_string(self):
        r = compress("")
        assert r.output == ""
        assert r.stats.saved == 0

    def test_whitespace_only(self):
        r = compress("   \n\n   ")
        assert r.stats.input_tokens >= 0

    def test_no_markdown(self):
        text = "The quick brown fox jumps over the lazy dog."
        r = compress(text)
        assert r.output  # non-empty

    def test_single_word(self):
        r = compress("Hello")
        assert "Hello" in r.output


# ---------------------------------------------------------------------------
# 3. Code block preservation
# ---------------------------------------------------------------------------

class TestCodeBlockPreservation:
    SNIPPET = textwrap.dedent("""\
        Here is some code:

        ```python
        def greet():
            # In order to say hello
            print("Hello world")
        ```

        End of example.
    """)

    def test_code_block_preserved(self):
        out = c(self.SNIPPET, "aggressive")
        assert "def greet():" in out
        assert 'print("Hello world")' in out

    def test_code_comments_preserved(self):
        out = c(self.SNIPPET, "aggressive")
        assert "# In order to say hello" in out

    def test_inline_code_preserved(self):
        text = "Use `In order to` literally here."
        out = c(text, "aggressive")
        assert "`In order to`" in out

    def test_fenced_code_language_tag_preserved(self):
        text = "```javascript\nconsole.log('hi')\n```"
        out = c(text)
        assert "javascript" in out
        assert "console.log" in out


# ---------------------------------------------------------------------------
# 4. Compression levels produce expected ordering
# ---------------------------------------------------------------------------

class TestCompressionLevels:
    LONG_TEXT = textwrap.dedent("""\
        # Introduction

        In order to understand this document, it is important to note that
        the following information provides a comprehensive overview of the
        subject matter. Due to the fact that this is a lengthy explanation,
        it is worth mentioning that readers should take note of the key points.

        ## Background

        It is important to understand that this section covers the background.
        Please note that the details below are provided for informational purposes.
        In the event that you have questions, do not hesitate to reach out.

        ## Conclusion

        In conclusion, it is clear that this document demonstrates the importance
        of concise writing. As a result of these findings, it can be seen that
        verbose text wastes tokens. The implementation of compression reduces costs.
    """)

    def test_levels_accepted(self):
        for lvl in ("light", "medium", "aggressive"):
            r = compress(self.LONG_TEXT, level=lvl)  # type: ignore[arg-type]
            assert r.output

    def test_aggressive_saves_more_than_light(self):
        s_light = stats(self.LONG_TEXT, "light")
        s_agg = stats(self.LONG_TEXT, "aggressive")
        assert s_agg.output_tokens <= s_light.output_tokens

    def test_medium_between_light_and_aggressive(self):
        s_light = stats(self.LONG_TEXT, "light")
        s_med = stats(self.LONG_TEXT, "medium")
        s_agg = stats(self.LONG_TEXT, "aggressive")
        assert s_agg.output_tokens <= s_med.output_tokens <= s_light.output_tokens

    def test_stats_level_field(self):
        for lvl in ("light", "medium", "aggressive"):
            s = stats(self.LONG_TEXT, lvl)
            assert s.level == lvl

    def test_invalid_level_raises(self):
        with pytest.raises((ValueError, KeyError, TypeError)):
            compress("hello", level="ultra_max")  # type: ignore[arg-type]


# ---------------------------------------------------------------------------
# 5. Verbose pattern replacement
# ---------------------------------------------------------------------------

class TestVerbosePatterns:
    @pytest.mark.parametrize("phrase,level,expected", [
        # medium-level patterns
        ("In order to do this",           "medium",     "To do this"),
        ("Due to the fact that it works", "medium",     "Because it works"),
        ("It is important to note that",  "medium",     ""),
        ("Please note that the value",    "medium",     "The value"),
        ("In addition to",                "medium",     "Also"),
        ("at this point in time",         "medium",     "now"),
        ("In the event that it fails",    "medium",     "If it fails"),
        # "As a result of" → "Due to" (not "So") — verify shortened form
        ("As a result of this,",          "medium",     "Due to this"),
        # aggressive-level patterns
        ("In conclusion,",                "aggressive",  ""),
        # synonym replacements
        ("subsequent to the change",      "medium",     "next to the change"),
    ])
    def test_verbose_phrase(self, phrase: str, level: str, expected: str):
        out = c(phrase + " we proceed.", level).strip()
        if expected:
            assert expected.lower() in out.lower(), f"Expected '{expected}' in output: {out!r}"
        else:
            # Empty expected = leading phrase should be removed/elided
            first_words = out.lower().split()[:3]
            assert phrase.lower().split()[0] not in first_words, \
                f"Phrase '{phrase}' should be removed, got: {out!r}"

    def test_saves_tokens_on_verbose_text(self):
        text = ("In order to understand this, it is important to note that "
                "due to the fact that we are here, it is worth mentioning the details. "
                "Please note that in addition to the above, it is clear that we should proceed.")
        s = stats(text, "medium")
        assert s.saved > 0


# ---------------------------------------------------------------------------
# 6. Table compression
# ---------------------------------------------------------------------------

class TestTableCompression:
    TABLE = textwrap.dedent("""\
        | Name  | Age | City     |
        |-------|-----|----------|
        | Alice | 30  | New York |
        | Bob   | 25  | London   |
    """)

    def test_table_compresses(self):
        s = stats(self.TABLE, "medium")
        assert s.output_tokens < s.input_tokens

    def test_table_data_preserved(self):
        out = c(self.TABLE, "medium")
        assert "Alice" in out
        assert "30" in out
        assert "New York" in out

    def test_two_column_table(self):
        table = "| Key | Value |\n|-----|-------|\n| foo | bar   |\n"
        out = c(table, "medium").lower()
        assert "foo" in out
        assert "bar" in out


# ---------------------------------------------------------------------------
# 7. List compression (aggressive only)
# ---------------------------------------------------------------------------

class TestListCompression:
    def test_short_list_collapsed(self):
        text = "- apple\n- banana\n- cherry\n"
        out = c(text, "aggressive")
        # Should collapse into a single line or significantly shorter
        assert out.count("\n") < text.count("\n")

    def test_long_list_items_preserved(self):
        text = textwrap.dedent("""\
            - This is a rather long bullet point that contains a lot of information
            - Another long bullet with substantial content that should not be lost
            - Yet another detailed item explaining something important for context
        """)
        out = c(text, "aggressive")
        # Content should not disappear
        assert "long bullet" in out or "substantial" in out


# ---------------------------------------------------------------------------
# 8. Heading compression
# ---------------------------------------------------------------------------

class TestHeadingCompression:
    def test_bold_in_heading_stripped(self):
        text = "## **Bold Heading**\n\nContent here."
        out = c(text, "medium")
        assert "**" not in out
        assert "Bold Heading" in out

    def test_deep_headings_reduced(self):
        text = "##### Very Deep Heading\n\nContent."
        out = c(text, "aggressive")
        # Deep heading should be reduced (e.g., ##### → ####, or lower)
        assert "#" in out

    def test_heading_text_preserved(self):
        text = "## My Section Title\n\nSome content."
        out = c(text, "medium")
        assert "My Section Title" in out


# ---------------------------------------------------------------------------
# 9. Whitespace compression
# ---------------------------------------------------------------------------

class TestWhitespaceCompression:
    def test_multiple_blank_lines_collapsed(self):
        text = "Line one.\n\n\n\nLine two."
        out = c(text, "light")
        assert "\n\n\n" not in out

    def test_trailing_spaces_removed(self):
        text = "Hello   \nWorld   \n"
        out = c(text, "light")
        for line in out.splitlines():
            assert line == line.rstrip()

    def test_horizontal_rules_stripped(self):
        text = "Section one.\n\n---\n\nSection two."
        out = c(text, "medium")
        assert "---" not in out


# ---------------------------------------------------------------------------
# 10. Link and image compression
# ---------------------------------------------------------------------------

class TestLinkCompression:
    def test_empty_link_title_stripped(self):
        text = "See [Click here](https://example.com) for details."
        out = c(text, "medium")
        # "Click here" is a filler link label; it may be simplified
        assert "example.com" in out or "https://" in out

    def test_plain_url_unchanged(self):
        text = "Visit https://example.com for info."
        out = c(text)
        assert "example.com" in out

    def test_image_alt_preserved(self):
        text = "![A diagram showing the architecture](arch.png)"
        out = c(text, "medium")
        assert "arch.png" in out


# ---------------------------------------------------------------------------
# 11. Token estimation
# ---------------------------------------------------------------------------

class TestTokenEstimation:
    def test_estimate_empty(self):
        assert estimate_tokens("") == 0

    def test_estimate_positive(self):
        assert estimate_tokens("Hello world this is a test.") > 0

    def test_estimate_scales_with_length(self):
        short = estimate_tokens("Hello.")
        long_ = estimate_tokens("Hello " * 100)
        assert long_ > short

    def test_estimate_approx_range(self):
        # "Hello world" ≈ 2-3 tokens
        t = estimate_tokens("Hello world")
        assert 1 <= t <= 5

    def test_stats_tokens_consistent_with_estimate(self):
        text = "The quick brown fox jumps over the lazy dog."
        r = compress(text)
        # input_tokens should match standalone estimate
        assert r.stats.input_tokens == estimate_tokens(text)


# ---------------------------------------------------------------------------
# 12. Stats accuracy
# ---------------------------------------------------------------------------

class TestStatsAccuracy:
    def test_saved_equals_input_minus_output(self):
        text = "In order to understand this, please note that it is important."
        s = stats(text, "medium")
        assert s.saved == s.input_tokens - s.output_tokens

    def test_pct_in_range(self):
        text = "In order to proceed, it is important to note the following details."
        s = stats(text, "medium")
        assert 0 <= s.pct <= 100

    def test_char_counts(self):
        text = "Hello, world! This is a test."
        s = stats(text)
        assert s.input_chars == len(text)
        assert s.output_chars >= 0

    def test_no_inflation(self):
        # Output should never be longer (in tokens) than input for any level
        texts = [
            "Hello.",
            "# Title\n\nContent.",
            "Short text with no verbose phrases.",
        ]
        for text in texts:
            for lvl in ("light", "medium", "aggressive"):
                s = stats(text, lvl)
                assert s.output_tokens <= s.input_tokens, \
                    f"Inflation for level={lvl}: {s.input_tokens} → {s.output_tokens}"


# ---------------------------------------------------------------------------
# 13. Real-world markdown samples
# ---------------------------------------------------------------------------

class TestRealWorldSamples:
    README_LIKE = textwrap.dedent("""\
        # My Project

        In order to get started with this project, it is important to note that
        you will need to install the dependencies first.

        ## Installation

        Please note that the following command should be run from the project root:

        ```bash
        npm install
        ```

        ## Usage

        Due to the fact that this library provides a comprehensive set of features,
        it is worth mentioning the most commonly used ones below.

        ### Basic Usage

        ```javascript
        const result = myLib.process(input)
        console.log(result)
        ```

        ## Contributing

        In the event that you would like to contribute, please note that all
        contributions are welcome. In addition to code, documentation improvements
        are also appreciated.

        | Feature     | Status   |
        |-------------|----------|
        | Compression | Done     |
        | CLI         | Done     |
        | Tests       | Done     |
    """)

    def test_readme_compresses(self):
        s = stats(self.README_LIKE, "medium")
        assert s.pct > 5  # Should save at least 5%

    def test_readme_code_preserved(self):
        out = c(self.README_LIKE, "aggressive")
        assert "npm install" in out
        assert "console.log" in out

    def test_readme_headings_preserved(self):
        out = c(self.README_LIKE, "medium")
        assert "Installation" in out
        assert "Usage" in out

    CHANGELOG_LIKE = textwrap.dedent("""\
        ## [1.2.0] - 2024-01-15

        ### Added

        - In order to support new users, added onboarding flow
        - Due to the fact that performance was slow, added caching layer
        - It is important to note that breaking changes were introduced

        ### Fixed

        - Please note that the bug in the parser has been fixed
        - In addition to the above, memory leaks have been addressed
    """)

    def test_changelog_compresses(self):
        s = stats(self.CHANGELOG_LIKE, "medium")
        assert s.saved > 0

    def test_changelog_version_preserved(self):
        out = c(self.CHANGELOG_LIKE, "medium")
        assert "1.2.0" in out


# ---------------------------------------------------------------------------
# 14. Passive voice compression (aggressive)
# ---------------------------------------------------------------------------

class TestPassiveVoice:
    def test_passive_voice_simplified(self):
        text = "The results were analyzed by the team and the findings were reported."
        s_none = estimate_tokens(text)
        out = c(text, "aggressive")
        s_out = estimate_tokens(out)
        # May or may not trigger depending on pattern match, just ensure no crash
        assert isinstance(out, str)

    def test_passive_voice_no_crash(self):
        text = "This is being processed. The data was sent. Results were found."
        out = c(text, "aggressive")
        assert out  # Non-empty output


# ---------------------------------------------------------------------------
# 15. Dictionary deduplication
# ---------------------------------------------------------------------------

class TestDictionaryDedup:
    def test_repeated_phrases_deduped(self):
        phrase = "machine learning"
        text = (f"We use {phrase} for classification. "
                f"The {phrase} model was trained. "
                f"Our {phrase} pipeline is scalable. "
                f"Every {phrase} system needs data.")
        out = c(text, "aggressive")
        # Either §1 symbol used OR the phrase is repeated (dedup is threshold-based)
        assert isinstance(out, str)
        assert len(out) > 0

    def test_dedup_stat_reported(self):
        phrase = "natural language processing"
        text = " ".join([f"The {phrase} task requires {phrase} models."] * 4)
        s = stats(text, "aggressive")
        # dictionary field should be >= 0
        assert s.dictionary >= 0


# ---------------------------------------------------------------------------
# 16. Article stripping (aggressive)
# ---------------------------------------------------------------------------

class TestArticleStripping:
    def test_articles_stripped_in_headings(self):
        text = "## The Introduction\n\nContent here."
        out = c(text, "aggressive")
        # "The" should be removed from heading
        lines = out.splitlines()
        heading = next((l for l in lines if l.startswith("#")), "")
        assert "The Introduction" not in heading or "Introduction" in heading

    def test_content_not_gutted(self):
        text = "## The Core Concept\n\nThe system processes the input data.\n"
        out = c(text, "aggressive").lower()
        assert "system" in out
        assert "input" in out


# ---------------------------------------------------------------------------
# 17. CLI smoke test (import only)
# ---------------------------------------------------------------------------

class TestCLI:
    def test_cli_importable(self):
        from mdmin._cli import main  # noqa: F401
        assert callable(main)

    def test_compress_cmd_to_stdout(self, tmp_path, capsys):
        from mdmin._cli import _cmd_compress
        import argparse
        md_file = tmp_path / "test.md"
        md_file.write_text("In order to test this, please note the output.\n")
        args = argparse.Namespace(file=str(md_file), level="medium", output=None)
        _cmd_compress(args)
        captured = capsys.readouterr()
        assert captured.out  # Something was printed

    def test_stats_cmd(self, tmp_path, capsys):
        from mdmin._cli import _cmd_stats
        import argparse
        md_file = tmp_path / "test.md"
        md_file.write_text("In order to test this, please note the output.\n")
        args = argparse.Namespace(file=str(md_file))
        _cmd_stats(args)
        captured = capsys.readouterr()
        assert "light" in captured.out
        assert "medium" in captured.out
        assert "aggressive" in captured.out
