import sys
from unittest.mock import MagicMock

import pytest

import octowrap.rewrap as mod

# Accessing _getch directly is needed to test the platform specific implementation.
# noinspection PyProtectedMember
from octowrap.rewrap import (
    _getch,
    colorize,
    prompt_user,
    show_block_diff,
)


class TestColorize:
    def test_known_color(self, monkeypatch):
        monkeypatch.setattr(mod, "_USE_COLOR", True)
        result = colorize("hello", "red")
        assert result == "\033[91mhello\033[0m"

    def test_bold(self, monkeypatch):
        monkeypatch.setattr(mod, "_USE_COLOR", True)
        result = colorize("title", "bold")
        assert result == "\033[1mtitle\033[0m"

    def test_unknown_color_still_resets(self, monkeypatch):
        monkeypatch.setattr(mod, "_USE_COLOR", True)
        result = colorize("text", "nonexistent")
        assert result == "text\033[0m"

    def test_empty_string(self, monkeypatch):
        monkeypatch.setattr(mod, "_USE_COLOR", True)
        result = colorize("", "green")
        assert result == "\033[92m\033[0m"

    def test_disabled_returns_plain_text(self, monkeypatch):
        monkeypatch.setattr(mod, "_USE_COLOR", False)
        assert colorize("hello", "red") == "hello"
        assert colorize("title", "bold") == "title"
        assert colorize("", "green") == ""


class TestShowBlockDiff:
    def test_no_changes_returns_false(self, capsys):
        lines = ["# hello world"]
        assert show_block_diff(lines, lines, 0) is False
        assert capsys.readouterr().out == ""

    def test_changes_returns_true(self, capsys):
        original = ["# hello", "# world"]
        new = ["# hello world"]
        assert show_block_diff(original, new, 0) is True

    def test_output_contains_original_and_new(self, capsys):
        original = ["# old line"]
        new = ["# new line"]
        show_block_diff(original, new, 5)
        out = capsys.readouterr().out
        assert "- # old line" in out
        assert "+ # new line" in out

    def test_output_contains_line_numbers(self, capsys):
        original = ["# a"]
        new = ["# b"]
        show_block_diff(original, new, 9)
        out = capsys.readouterr().out
        # start_line is 0 indexed, display is 1 indexed
        assert "Lines 10-10:" in out

    def test_output_contains_filepath(self, capsys):
        original = ["# a"]
        new = ["# b"]
        show_block_diff(original, new, 0, filepath="src/example.py")
        out = capsys.readouterr().out
        assert "src/example.py" in out
        assert "Lines 1-1:" in out

    def test_no_filepath_omits_prefix(self, capsys):
        original = ["# a"]
        new = ["# b"]
        show_block_diff(original, new, 0)
        out = capsys.readouterr().out
        assert out.startswith("\n") or "Lines 1-1:" in out
        # Should not have a path prefix before "Lines"
        for line in out.splitlines():
            if "Lines" in line:
                stripped = line.lstrip()
                # Remove ANSI codes for comparison
                import re

                clean = re.sub(r"\033\[[0-9;]*m", "", stripped)
                assert clean.startswith("Lines")


class TestGetch:
    """Tests for _getch(), the platform specific single keypress reader.

    _getch uses msvcrt on Windows and termios/tty on Unix (imported locally inside the
    function).  The skipif markers ensure each platform native test only runs where the
    real modules exist. test_non_native_platform covers the opposite branch by
    monkeypatching sys.platform and injecting fake modules into sys.modules.
    """

    @pytest.mark.skipif(sys.platform != "win32", reason="Windows only path")
    def test_windows_msvcrt(self, monkeypatch):
        """On Windows, _getch delegates to msvcrt.getwch."""
        import msvcrt

        monkeypatch.setattr(msvcrt, "getwch", lambda: "k")
        assert _getch() == "k"

    @pytest.mark.skipif(sys.platform == "win32", reason="Unix only path")
    def test_unix_termios(self, monkeypatch):
        """On Unix, _getch uses termios/tty to read one character."""
        import termios
        import tty

        monkeypatch.setattr("sys.stdin.fileno", lambda: 0)
        monkeypatch.setattr("sys.stdin.read", lambda n: "k")
        monkeypatch.setattr(termios, "tcgetattr", lambda fd: [])
        monkeypatch.setattr(termios, "tcsetattr", lambda fd, when, old: None)
        monkeypatch.setattr(termios, "TCSADRAIN", 1)
        monkeypatch.setattr(tty, "setcbreak", lambda fd: None)

        assert _getch() == "k"

    def test_non_native_platform(self, monkeypatch):
        """Covers the branch for the platform we are NOT running on.

        Since _getch imports platform modules locally, we inject fake modules into
        sys.modules so the import statement picks them up.
        """
        if sys.platform == "win32":
            # We're on Windows, so fake the Unix path.
            monkeypatch.setattr(sys, "platform", "linux")
            fake_termios = MagicMock()
            fake_termios.tcgetattr.return_value = []
            monkeypatch.setitem(sys.modules, "termios", fake_termios)
            monkeypatch.setitem(sys.modules, "tty", MagicMock())
            monkeypatch.setattr(
                "sys.stdin",
                MagicMock(
                    fileno=MagicMock(return_value=0), read=MagicMock(return_value="z")
                ),
            )
            assert _getch() == "z"
        else:
            # We're on Unix, so fake the Windows path.
            monkeypatch.setattr(sys, "platform", "win32")
            fake_msvcrt = MagicMock()
            fake_msvcrt.getwch.return_value = "z"
            monkeypatch.setitem(sys.modules, "msvcrt", fake_msvcrt)
            assert _getch() == "z"


class TestPromptUser:
    def test_accept(self, monkeypatch):
        monkeypatch.setattr(mod, "_getch", lambda: "a")
        assert prompt_user() == "a"

    def test_skip(self, monkeypatch):
        monkeypatch.setattr(mod, "_getch", lambda: "s")
        assert prompt_user() == "s"

    def test_quit(self, monkeypatch):
        monkeypatch.setattr(mod, "_getch", lambda: "q")
        assert prompt_user() == "q"

    def test_accept_all(self, monkeypatch):
        monkeypatch.setattr(mod, "_getch", lambda: "A")
        assert prompt_user() == "A"

    def test_lowercase_a_is_single_accept(self, monkeypatch):
        monkeypatch.setattr(mod, "_getch", lambda: "a")
        assert prompt_user() == "a"

    def test_invalid_then_valid(self, monkeypatch):
        responses = iter(["x", "z", "a"])
        monkeypatch.setattr(mod, "_getch", lambda: next(responses))
        assert prompt_user() == "a"

    def test_eof_returns_quit(self, monkeypatch):
        def raise_eof():
            raise EOFError

        monkeypatch.setattr(mod, "_getch", raise_eof)
        assert prompt_user() == "q"

    def test_keyboard_interrupt_returns_quit(self, monkeypatch):
        def raise_interrupt():
            raise KeyboardInterrupt

        monkeypatch.setattr(mod, "_getch", raise_interrupt)
        assert prompt_user() == "q"

    def test_exclude(self, monkeypatch):
        monkeypatch.setattr(mod, "_getch", lambda: "e")
        assert prompt_user() == "e"

    def test_exclude_uppercase(self, monkeypatch):
        monkeypatch.setattr(mod, "_getch", lambda: "E")
        assert prompt_user() == "e"

    def test_prompt_colors_are_all_valid(self, monkeypatch):
        """Every colorize() call in prompt_user must use a known color name."""
        monkeypatch.setattr(mod, "_USE_COLOR", True)
        monkeypatch.setattr(mod, "_getch", lambda: "a")
        captured = []
        original_colorize = colorize

        def spy(text, this_color):
            captured.append(this_color)
            return original_colorize(text, this_color)

        monkeypatch.setattr(mod, "colorize", spy)
        prompt_user()

        colors = {"red", "green", "yellow", "cyan", "magenta", "bold", "reset"}
        for color in captured:
            assert color in colors, f"prompt_user uses unknown color {color!r}"
