"""Tests for pygame editor functionality."""

import pytest
import sys
from unittest.mock import MagicMock, patch

# Mock pygame before importing the editor
pygame_mock = MagicMock()
pygame_mock.K_a = 97
pygame_mock.K_b = 98
pygame_mock.K_c = 99
pygame_mock.K_e = 101
pygame_mock.K_r = 114
pygame_mock.K_y = 121
pygame_mock.K_z = 122
pygame_mock.KMOD_CTRL = 64
pygame_mock.KMOD_SHIFT = 1
pygame_mock.KMOD_ALT = 256
sys.modules['pygame'] = pygame_mock
sys.modules['pygame.freetype'] = MagicMock()
sys.modules['pygame_ce'] = pygame_mock

from e3po.editor import Buffer, UndoType, UndoEntry, MarkMode, Mark


class MockPygameEditor:
    """
    A minimal mock of PygameEditor for testing logic without pygame GUI.
    Tests the same methods that PygameEditor uses.
    """
    def __init__(self, lines=None):
        self.buffer = Buffer()
        if lines:
            self.buffer.lines = lines.copy()
        self.cursor_row = 0
        self.cursor_col = 0
        self.message = ""
        self.mark = None
        self.syntax_cache = {}

    def _invalidate_syntax_cache(self):
        self.syntax_cache = {}

    def _undo(self):
        """Undo last operation."""
        entry = self.buffer.pop_undo()
        if entry:
            end_line = min(entry.line_num + entry.line_count, len(self.buffer.lines))
            current_lines = self.buffer.lines[entry.line_num:end_line] if entry.line_num < len(self.buffer.lines) else []
            self.buffer.redo_stack.append(UndoEntry(
                entry.undo_type, entry.line_num, self.cursor_col,
                current_lines,
                len(current_lines)
            ))

            if entry.undo_type == UndoType.LINE_DELETE:
                for i, line in enumerate(entry.saved_lines):
                    self.buffer.lines.insert(entry.line_num + i, line)
            elif entry.undo_type == UndoType.LINE_INSERT:
                end = min(entry.line_num + entry.line_count, len(self.buffer.lines))
                if entry.line_num < len(self.buffer.lines) and end > entry.line_num:
                    del self.buffer.lines[entry.line_num:end]
            elif entry.undo_type == UndoType.LINE_CHANGE:
                for i, line in enumerate(entry.saved_lines):
                    if entry.line_num + i < len(self.buffer.lines):
                        self.buffer.lines[entry.line_num + i] = line
            elif entry.undo_type == UndoType.BLOCK_CHANGE:
                if entry.col_pos < 0:
                    original_line_count = -entry.col_pos - 1
                    if len(self.buffer.lines) > original_line_count:
                        del self.buffer.lines[original_line_count:]
                for i, line in enumerate(entry.saved_lines):
                    if entry.line_num + i < len(self.buffer.lines):
                        self.buffer.lines[entry.line_num + i] = line

            if not self.buffer.lines:
                self.buffer.lines = [""]
            self.mark = None
            self.cursor_row = min(entry.line_num, len(self.buffer.lines) - 1)
            self._invalidate_syntax_cache()
            self.message = "Undo"
        else:
            self.message = "Nothing to undo"

    def _redo(self):
        """Redo last undone operation."""
        if not self.buffer.redo_stack:
            self.message = "Nothing to redo"
            return

        entry = self.buffer.redo_stack.pop()
        self.buffer.push_undo(
            entry.undo_type, entry.line_num, self.cursor_col,
            entry.line_num, entry.line_num + entry.line_count - 1
        )
        self._invalidate_syntax_cache()
        self.message = "Redo"


class TestPygameRedo:
    """Test redo functionality in pygame editor."""

    def test_redo_after_undo(self):
        """Ctrl-R or Ctrl-Y should redo after undo."""
        editor = MockPygameEditor(["hello"])
        # Make a change
        editor.buffer.push_undo(UndoType.LINE_CHANGE, 0, 0, 0, 0)
        editor.buffer.lines[0] = "hello world"

        # Undo it
        editor._undo()
        assert editor.buffer.lines[0] == "hello"

        # Redo it (this is what Ctrl-R triggers)
        editor._redo()
        assert editor.message == "Redo"

    def test_redo_nothing_to_redo(self):
        """Redo with empty redo stack shows message."""
        editor = MockPygameEditor(["hello"])
        editor._redo()
        assert editor.message == "Nothing to redo"

    def test_redo_clears_on_new_edit(self):
        """New edits clear the redo stack."""
        editor = MockPygameEditor(["hello"])
        editor.buffer.push_undo(UndoType.LINE_CHANGE, 0, 0, 0, 0)
        editor.buffer.lines[0] = "hello world"

        editor._undo()
        assert len(editor.buffer.redo_stack) == 1

        # New edit clears redo
        editor.buffer.push_undo(UndoType.LINE_CHANGE, 0, 0, 0, 0)
        assert len(editor.buffer.redo_stack) == 0


class TestPygameUndo:
    """Test undo functionality in pygame editor."""

    def test_undo_clears_mark(self):
        """Undo should clear any active mark."""
        editor = MockPygameEditor(["hello", "world"])
        editor.mark = Mark(mode=MarkMode.LINE, start_row=0, start_col=0)

        editor.buffer.push_undo(UndoType.LINE_CHANGE, 0, 0, 0, 0)
        editor.buffer.lines[0] = "modified"

        editor._undo()
        assert editor.mark is None

    def test_undo_invalidates_syntax_cache(self):
        """Undo should invalidate syntax cache."""
        editor = MockPygameEditor(["hello"])
        editor.syntax_cache = {(0, "hello"): {0: 1}}

        editor.buffer.push_undo(UndoType.LINE_CHANGE, 0, 0, 0, 0)
        editor.buffer.lines[0] = "modified"

        editor._undo()
        assert editor.syntax_cache == {}


class MockPygameEditorWithSort(MockPygameEditor):
    """Extended mock with sort command support."""

    def _do_sort_command(self, cmd: str) -> None:
        """Execute sort command on marked lines or entire file."""
        import re

        args = cmd[4:].strip() if len(cmd) > 4 else ""
        reverse = '-r' in args
        numeric = '-n' in args
        fold_case = '-f' in args

        field_nums = [int(m.group(1)) for m in re.finditer(r'-k(\d+)', args)]

        if self.mark:
            start_row = min(self.mark.start_row, self.mark.end_row if self.mark.end_row is not None else self.mark.start_row)
            end_row = max(self.mark.start_row, self.mark.end_row if self.mark.end_row is not None else self.mark.start_row)
        else:
            start_row = 0
            end_row = len(self.buffer.lines) - 1

        lines = [self.buffer.get_line(row) for row in range(start_row, end_row + 1)]

        if self.mark and self.mark.mode == MarkMode.BLOCK:
            start_col = min(self.mark.start_col, self.mark.end_col if self.mark.end_col is not None else self.mark.start_col)
            end_col = max(self.mark.start_col, self.mark.end_col if self.mark.end_col is not None else self.mark.start_col)

            def get_key(line):
                key = line[start_col:end_col + 1] if len(line) > start_col else ""
                if fold_case:
                    key = key.lower()
                if numeric:
                    try:
                        return float(key.strip())
                    except ValueError:
                        return float('-inf') if not reverse else float('inf')
                return key
        elif field_nums:
            def get_key(line):
                fields = line.split()
                keys = []
                for field_num in field_nums:
                    if field_num <= len(fields):
                        key = fields[field_num - 1]
                    else:
                        key = ""
                    if fold_case:
                        key = key.lower()
                    if numeric:
                        try:
                            keys.append(float(key.strip()))
                        except ValueError:
                            keys.append(float('-inf') if not reverse else float('inf'))
                    else:
                        keys.append(key)
                return tuple(keys) if len(keys) > 1 else keys[0] if keys else ""
        else:
            def get_key(line):
                key = line
                if fold_case:
                    key = key.lower()
                if numeric:
                    try:
                        return float(key.strip())
                    except ValueError:
                        return float('-inf') if not reverse else float('inf')
                return key

        sorted_lines = sorted(lines, key=get_key, reverse=reverse)

        self.buffer.push_undo(UndoType.BLOCK_CHANGE, start_row, 0, start_row, end_row)

        for i, row in enumerate(range(start_row, end_row + 1)):
            if i < len(sorted_lines):
                self.buffer.set_line(row, sorted_lines[i])

        self.buffer.modified = True
        self.mark = None
        self._invalidate_syntax_cache()
        count = end_row - start_row + 1
        self.message = f"Sorted {count} line{'s' if count != 1 else ''}"


class TestPygameSort:
    """Test sort command in pygame editor."""

    def test_sort_entire_file(self):
        """Sort entire file without mark."""
        editor = MockPygameEditorWithSort(["cherry", "apple", "banana"])
        editor._do_sort_command("sort")
        assert editor.buffer.lines == ["apple", "banana", "cherry"]

    def test_sort_reverse(self):
        """Sort in reverse order."""
        editor = MockPygameEditorWithSort(["apple", "banana", "cherry"])
        editor._do_sort_command("sort -r")
        assert editor.buffer.lines == ["cherry", "banana", "apple"]

    def test_sort_numeric(self):
        """Numeric sort."""
        editor = MockPygameEditorWithSort(["10", "2", "1", "20"])
        editor._do_sort_command("sort -n")
        assert editor.buffer.lines == ["1", "2", "10", "20"]

    def test_sort_case_insensitive(self):
        """Case insensitive sort."""
        editor = MockPygameEditorWithSort(["Banana", "apple", "Cherry"])
        editor._do_sort_command("sort -f")
        assert editor.buffer.lines[0].lower() == "apple"

    def test_sort_marked_lines_only(self):
        """Sort only marked lines."""
        editor = MockPygameEditorWithSort(["header", "cherry", "apple", "banana", "footer"])
        editor.mark = Mark(
            mode=MarkMode.LINE,
            start_row=1,
            start_col=0,
            end_row=3,
            end_col=0,
            pending=False
        )
        editor._do_sort_command("sort")
        assert editor.buffer.lines == ["header", "apple", "banana", "cherry", "footer"]

    def test_sort_clears_mark(self):
        """Sort should clear the mark."""
        editor = MockPygameEditorWithSort(["cherry", "apple", "banana"])
        editor.mark = Mark(mode=MarkMode.LINE, start_row=0, start_col=0, end_row=2, end_col=0)
        editor._do_sort_command("sort")
        assert editor.mark is None

    def test_sort_by_field(self):
        """Sort by field number."""
        editor = MockPygameEditorWithSort(["zebra 1", "apple 3", "mango 2"])
        editor._do_sort_command("sort -k2")
        assert editor.buffer.lines == ["zebra 1", "mango 2", "apple 3"]

    def test_sort_invalidates_syntax_cache(self):
        """Sort should invalidate syntax cache."""
        editor = MockPygameEditorWithSort(["cherry", "apple"])
        editor.syntax_cache = {(0, "cherry"): {0: 1}}
        editor._do_sort_command("sort")
        assert editor.syntax_cache == {}

    def test_sort_marks_modified(self):
        """Sort should mark buffer as modified."""
        editor = MockPygameEditorWithSort(["cherry", "apple"])
        editor.buffer.modified = False
        editor._do_sort_command("sort")
        assert editor.buffer.modified == True

    def test_sort_creates_undo(self):
        """Sort should create an undo entry."""
        editor = MockPygameEditorWithSort(["cherry", "apple", "banana"])
        original = editor.buffer.lines.copy()
        editor._do_sort_command("sort")
        assert editor.buffer.lines == ["apple", "banana", "cherry"]
        editor._undo()
        assert editor.buffer.lines == original


class MockPygameEditorWithUniq(MockPygameEditorWithSort):
    """Extended mock with uniq command support."""

    def _do_uniq_command(self, cmd: str) -> None:
        """Remove adjacent duplicate lines."""
        if cmd.upper().startswith('UNIQUE'):
            args = cmd[6:].strip()
        else:
            args = cmd[4:].strip()
        case_insensitive = '-i' in args

        if self.mark:
            start_row = min(self.mark.start_row, self.mark.end_row if self.mark.end_row is not None else self.mark.start_row)
            end_row = max(self.mark.start_row, self.mark.end_row if self.mark.end_row is not None else self.mark.start_row)
        else:
            start_row = 0
            end_row = len(self.buffer.lines) - 1

        lines = [self.buffer.get_line(row) for row in range(start_row, end_row + 1)]

        if not lines:
            self.message = "No lines to process"
            return

        result = [lines[0]]
        for line in lines[1:]:
            prev = result[-1]
            if case_insensitive:
                is_dup = line.lower() == prev.lower()
            else:
                is_dup = line == prev
            if not is_dup:
                result.append(line)

        removed = len(lines) - len(result)
        if removed == 0:
            self.message = "No duplicates found"
            return

        self.buffer.push_undo(UndoType.BLOCK_CHANGE, start_row, 0, start_row, end_row)

        for _ in range(start_row, end_row + 1):
            if start_row < len(self.buffer.lines):
                del self.buffer.lines[start_row]

        for i, line in enumerate(result):
            self.buffer.lines.insert(start_row + i, line)

        if not self.buffer.lines:
            self.buffer.lines = [""]

        self.buffer.modified = True
        self.mark = None
        self._invalidate_syntax_cache()
        self.message = f"Removed {removed} duplicate{'s' if removed != 1 else ''}"


class TestPygameUniq:
    """Test uniq command in pygame editor."""

    def test_uniq_removes_adjacent_duplicates(self):
        """Uniq removes adjacent duplicate lines."""
        editor = MockPygameEditorWithUniq(["apple", "apple", "banana", "banana", "cherry"])
        editor._do_uniq_command("uniq")
        assert editor.buffer.lines == ["apple", "banana", "cherry"]

    def test_uniq_preserves_non_adjacent_duplicates(self):
        """Uniq keeps non-adjacent duplicates."""
        editor = MockPygameEditorWithUniq(["apple", "banana", "apple", "cherry"])
        editor._do_uniq_command("uniq")
        assert editor.buffer.lines == ["apple", "banana", "apple", "cherry"]

    def test_uniq_case_insensitive(self):
        """Uniq -i ignores case."""
        editor = MockPygameEditorWithUniq(["Apple", "apple", "APPLE", "banana"])
        editor._do_uniq_command("uniq -i")
        assert editor.buffer.lines == ["Apple", "banana"]

    def test_uniq_no_duplicates(self):
        """Uniq with no duplicates shows message."""
        editor = MockPygameEditorWithUniq(["apple", "banana", "cherry"])
        editor._do_uniq_command("uniq")
        assert editor.message == "No duplicates found"
        assert editor.buffer.lines == ["apple", "banana", "cherry"]

    def test_uniq_marked_lines_only(self):
        """Uniq only affects marked lines."""
        editor = MockPygameEditorWithUniq(["header", "a", "a", "b", "b", "footer"])
        editor.mark = Mark(
            mode=MarkMode.LINE,
            start_row=1,
            start_col=0,
            end_row=4,
            end_col=0,
            pending=False
        )
        editor._do_uniq_command("uniq")
        assert editor.buffer.lines == ["header", "a", "b", "footer"]

    def test_uniq_clears_mark(self):
        """Uniq clears the mark."""
        editor = MockPygameEditorWithUniq(["a", "a", "b"])
        editor.mark = Mark(mode=MarkMode.LINE, start_row=0, start_col=0, end_row=2, end_col=0)
        editor._do_uniq_command("uniq")
        assert editor.mark is None

    def test_uniq_marks_modified(self):
        """Uniq marks buffer as modified."""
        editor = MockPygameEditorWithUniq(["a", "a", "b"])
        editor.buffer.modified = False
        editor._do_uniq_command("uniq")
        assert editor.buffer.modified == True

    def test_uniq_invalidates_syntax_cache(self):
        """Uniq invalidates syntax cache."""
        editor = MockPygameEditorWithUniq(["a", "a"])
        editor.syntax_cache = {(0, "a"): {0: 1}}
        editor._do_uniq_command("uniq")
        assert editor.syntax_cache == {}

    def test_unique_long_form(self):
        """'unique' command works same as 'uniq'."""
        editor = MockPygameEditorWithUniq(["a", "a", "b"])
        editor._do_uniq_command("unique")
        assert editor.buffer.lines == ["a", "b"]


class MockPygameEditorWithGrep(MockPygameEditorWithUniq):
    """Extended mock with grep command support."""

    def _do_grep_command(self, cmd: str) -> None:
        """Filter lines using grep."""
        import re

        args_str = cmd[4:].strip()

        if not args_str:
            self.message = "Usage: grep [-v] [-i] pattern"
            return

        inverse = False
        case_insensitive = False

        parts = args_str.split()
        pattern = None

        for i, part in enumerate(parts):
            if part == '-v':
                inverse = True
            elif part == '-i':
                case_insensitive = True
            elif part == '-iv' or part == '-vi':
                inverse = True
                case_insensitive = True
            elif pattern is None:
                pattern = ' '.join(parts[i:])
                break

        if not pattern:
            self.message = "Usage: grep [-v] [-i] pattern"
            return

        flags = re.IGNORECASE if case_insensitive else 0
        try:
            regex = re.compile(pattern, flags)
        except re.error as e:
            self.message = f"Invalid pattern: {e}"
            return

        if self.mark:
            start_row = min(self.mark.start_row, self.mark.end_row if self.mark.end_row is not None else self.mark.start_row)
            end_row = max(self.mark.start_row, self.mark.end_row if self.mark.end_row is not None else self.mark.start_row)
        else:
            start_row = 0
            end_row = len(self.buffer.lines) - 1

        block_mode = self.mark and self.mark.mode == MarkMode.BLOCK
        if block_mode:
            start_col = min(self.mark.start_col, self.mark.end_col if self.mark.end_col is not None else self.mark.start_col)
            end_col = max(self.mark.start_col, self.mark.end_col if self.mark.end_col is not None else self.mark.start_col)

        original_lines = [self.buffer.get_line(row) for row in range(start_row, end_row + 1)]

        matching_indices = set()
        for i, line in enumerate(original_lines):
            if block_mode:
                search_text = line[start_col:end_col + 1] if len(line) > start_col else ""
            else:
                search_text = line
            if regex.search(search_text):
                matching_indices.add(i)

        if not matching_indices and not inverse:
            self.message = "No matches found"
            return

        if len(matching_indices) == len(original_lines) and inverse:
            self.message = "All lines match - nothing to keep"
            return

        if inverse:
            keep_indices = [i for i in range(len(original_lines)) if i not in matching_indices]
        else:
            keep_indices = [i for i in range(len(original_lines)) if i in matching_indices]

        kept_lines = [original_lines[i] for i in keep_indices]
        removed = len(original_lines) - len(kept_lines)

        if removed == 0:
            self.message = "No changes needed"
            return

        self.buffer.push_undo(UndoType.BLOCK_CHANGE, start_row, 0, start_row, end_row)

        for _ in range(start_row, end_row + 1):
            if start_row < len(self.buffer.lines):
                del self.buffer.lines[start_row]

        for i, line in enumerate(kept_lines):
            self.buffer.lines.insert(start_row + i, line)

        if not self.buffer.lines:
            self.buffer.lines = [""]

        self.buffer.modified = True
        self.mark = None
        self._invalidate_syntax_cache()
        kept = len(kept_lines)
        self.message = f"Kept {kept} line{'s' if kept != 1 else ''}, removed {removed}"


class TestPygameGrep:
    """Test grep command in pygame editor."""

    def test_grep_keeps_matching_lines(self):
        """Grep keeps only matching lines."""
        editor = MockPygameEditorWithGrep(["apple pie", "banana bread", "apple tart", "cherry"])
        editor._do_grep_command("grep apple")
        assert editor.buffer.lines == ["apple pie", "apple tart"]

    def test_grep_no_matches(self):
        """Grep with no matches shows message."""
        editor = MockPygameEditorWithGrep(["apple", "banana", "cherry"])
        editor._do_grep_command("grep xyz")
        assert editor.message == "No matches found"
        assert editor.buffer.lines == ["apple", "banana", "cherry"]

    def test_grep_inverse(self):
        """Grep -v keeps non-matching lines."""
        editor = MockPygameEditorWithGrep(["apple", "banana", "cherry", "apple pie"])
        editor._do_grep_command("grep -v apple")
        assert editor.buffer.lines == ["banana", "cherry"]

    def test_grep_case_insensitive(self):
        """Grep -i ignores case."""
        editor = MockPygameEditorWithGrep(["Apple", "BANANA", "cherry"])
        editor._do_grep_command("grep -i apple")
        assert editor.buffer.lines == ["Apple"]

    def test_grep_combined_options(self):
        """Grep -iv works together."""
        editor = MockPygameEditorWithGrep(["Apple", "banana", "APPLE pie", "cherry"])
        editor._do_grep_command("grep -iv apple")
        assert editor.buffer.lines == ["banana", "cherry"]

    def test_grep_regex(self):
        """Grep supports regex patterns."""
        editor = MockPygameEditorWithGrep(["test123", "test456", "hello", "test"])
        editor._do_grep_command("grep test\\d+")
        assert editor.buffer.lines == ["test123", "test456"]

    def test_grep_marked_lines_only(self):
        """Grep only affects marked lines."""
        editor = MockPygameEditorWithGrep(["header", "apple", "banana", "apple pie", "footer"])
        editor.mark = Mark(
            mode=MarkMode.LINE,
            start_row=1,
            start_col=0,
            end_row=3,
            end_col=0,
            pending=False
        )
        editor._do_grep_command("grep apple")
        assert editor.buffer.lines == ["header", "apple", "apple pie", "footer"]

    def test_grep_clears_mark(self):
        """Grep clears the mark."""
        editor = MockPygameEditorWithGrep(["apple", "banana"])
        editor.mark = Mark(mode=MarkMode.LINE, start_row=0, start_col=0, end_row=1, end_col=0)
        editor._do_grep_command("grep apple")
        assert editor.mark is None

    def test_grep_marks_modified(self):
        """Grep marks buffer as modified."""
        editor = MockPygameEditorWithGrep(["apple", "banana"])
        editor.buffer.modified = False
        editor._do_grep_command("grep apple")
        assert editor.buffer.modified == True

    def test_grep_invalidates_syntax_cache(self):
        """Grep invalidates syntax cache."""
        editor = MockPygameEditorWithGrep(["apple", "banana"])
        editor.syntax_cache = {(0, "apple"): {0: 1}}
        editor._do_grep_command("grep apple")
        assert editor.syntax_cache == {}

    def test_grep_empty_pattern(self):
        """Grep with no pattern shows usage."""
        editor = MockPygameEditorWithGrep(["apple", "banana"])
        editor._do_grep_command("grep")
        assert "Usage" in editor.message


class MockPygameEditorWithEncrypt(MockPygameEditorWithGrep):
    """Extended mock with encrypt/passwd support."""

    def __init__(self, lines=None):
        super().__init__(lines)
        self.passphrase_mode = False
        self.passphrase_buffer = ""
        self.passphrase_confirm = ""
        self.passphrase_step = 0
        self.passphrase_callback = None

    def _do_encrypt_command(self) -> None:
        """Start encryption."""
        if self.buffer.encrypted:
            self.message = "Already encrypted"
            return

        self.passphrase_mode = True
        self.passphrase_buffer = ""
        self.passphrase_confirm = ""
        self.passphrase_step = 0
        self.passphrase_callback = self._finish_encrypt
        self.message = "New passphrase: "

    def _do_passwd_command(self) -> None:
        """Start password change."""
        if not self.buffer.encrypted:
            self.message = "File is not encrypted"
            return

        self.passphrase_mode = True
        self.passphrase_buffer = ""
        self.passphrase_confirm = ""
        self.passphrase_step = 2
        self.passphrase_callback = self._finish_passwd
        self.message = "Current passphrase: "

    def _finish_encrypt(self, passphrase: str) -> None:
        """Complete encryption."""
        self.buffer.encrypted = True
        self.buffer.passphrase = passphrase
        self.buffer.modified = True
        self.message = "Encrypted. Save to apply."

    def _finish_passwd(self, passphrase: str) -> None:
        """Complete password change."""
        self.buffer.passphrase = passphrase
        self.buffer.modified = True
        self.message = "Passphrase changed. Save to apply."

    def _simulate_passphrase_entry(self, passphrase: str, confirm: str = None):
        """Simulate entering a passphrase (helper for tests)."""
        if confirm is None:
            confirm = passphrase

        # Enter passphrase
        self.passphrase_buffer = passphrase
        # Simulate Enter
        if self.passphrase_step == 0:
            self.passphrase_confirm = passphrase
            self.passphrase_buffer = ""
            self.passphrase_step = 1
            # Enter confirm
            self.passphrase_buffer = confirm
            if passphrase == confirm:
                self.passphrase_mode = False
                if self.passphrase_callback:
                    self.passphrase_callback(passphrase)
            else:
                self.passphrase_mode = False
                self.message = "Passphrases don't match"


class TestPygameEncrypt:
    """Test encrypt command in pygame editor."""

    def test_encrypt_starts_passphrase_mode(self):
        """Encrypt command enters passphrase mode."""
        editor = MockPygameEditorWithEncrypt(["secret data"])
        editor._do_encrypt_command()
        assert editor.passphrase_mode == True
        assert "passphrase" in editor.message.lower()

    def test_encrypt_already_encrypted(self):
        """Encrypt on already encrypted file shows message."""
        editor = MockPygameEditorWithEncrypt(["secret"])
        editor.buffer.encrypted = True
        editor._do_encrypt_command()
        assert editor.passphrase_mode == False
        assert "Already encrypted" in editor.message

    def test_encrypt_sets_buffer_state(self):
        """Successful encryption sets buffer state."""
        editor = MockPygameEditorWithEncrypt(["secret"])
        editor._do_encrypt_command()
        editor._simulate_passphrase_entry("mypassword")
        assert editor.buffer.encrypted == True
        assert editor.buffer.passphrase == "mypassword"
        assert editor.buffer.modified == True

    def test_encrypt_mismatch_fails(self):
        """Mismatched passphrases fail encryption."""
        editor = MockPygameEditorWithEncrypt(["secret"])
        editor._do_encrypt_command()
        editor._simulate_passphrase_entry("pass1", "pass2")
        assert editor.buffer.encrypted == False
        assert "don't match" in editor.message


class TestPygamePasswd:
    """Test passwd command in pygame editor."""

    def test_passwd_requires_encrypted(self):
        """Passwd on unencrypted file shows error."""
        editor = MockPygameEditorWithEncrypt(["plain text"])
        editor._do_passwd_command()
        assert editor.passphrase_mode == False
        assert "not encrypted" in editor.message

    def test_passwd_starts_with_old(self):
        """Passwd starts by asking for current passphrase."""
        editor = MockPygameEditorWithEncrypt(["secret"])
        editor.buffer.encrypted = True
        editor.buffer.passphrase = "oldpass"
        editor._do_passwd_command()
        assert editor.passphrase_mode == True
        assert editor.passphrase_step == 2  # Old passphrase step
        assert "Current" in editor.message

    def test_passwd_changes_passphrase(self):
        """Successful passwd changes the passphrase."""
        editor = MockPygameEditorWithEncrypt(["secret"])
        editor.buffer.encrypted = True
        editor.buffer.passphrase = "oldpass"
        editor._finish_passwd("newpass")
        assert editor.buffer.passphrase == "newpass"
        assert editor.buffer.modified == True
        assert "changed" in editor.message.lower()


# Mock SpellChecker for testing
class MockSpellChecker:
    """Mock spell checker for testing."""
    def __init__(self):
        self.known_words = {'hello', 'world', 'the', 'is', 'a', 'test'}
        self.word_frequency = self

    def __contains__(self, word):
        return word.lower() in self.known_words

    def candidates(self, word):
        # Return some mock suggestions
        if word.lower() == 'helo':
            return ['hello', 'help', 'held']
        if word.lower() == 'wrold':
            return ['world', 'would']
        return []

    def load_words(self, words):
        for w in words:
            self.known_words.add(w.lower())


class MockPygameEditorWithSpell(MockPygameEditorWithEncrypt):
    """Extended mock with spell check support."""

    def __init__(self, lines=None):
        super().__init__(lines)
        self.spell_mode = False
        self.spell_checker = None
        self.spell_errors = []
        self.spell_index = -1
        self.spell_skip_words = set()
        self.spell_suggestions = []

    def _toggle_spell_mode(self) -> None:
        """Toggle spell mode."""
        if self.spell_mode:
            self._clear_spell_mode()
            self.message = ""
        else:
            self.spell_mode = True
            self._init_spell_checker()
            self._scan_spelling()
            count = len(self.spell_errors)
            if count == 0:
                self.message = "SPELL: No errors found | o)ff"
            else:
                self.message = f"SPELL: {count} error{'s' if count != 1 else ''} - n)ext p)rev o)ff"

    def _clear_spell_mode(self) -> None:
        self.spell_mode = False
        self.spell_errors = []
        self.spell_index = -1
        self.spell_suggestions = []

    def _init_spell_checker(self) -> None:
        if self.spell_checker is None:
            self.spell_checker = MockSpellChecker()

    def _scan_spelling(self) -> None:
        import re
        self.spell_errors = []
        if not self.spell_checker:
            return

        word_pattern = re.compile(r"[a-zA-Z']+")
        for row, line in enumerate(self.buffer.lines):
            for match in word_pattern.finditer(line):
                word = match.group()
                start_col = match.start()
                end_col = match.end()

                if len(word) < 2 or word.replace("'", "") == "":
                    continue
                if word.lower() in self.spell_skip_words:
                    continue
                if word.lower() not in self.spell_checker:
                    self.spell_errors.append((row, start_col, end_col, word))

    def _spell_next(self) -> None:
        if not self.spell_errors:
            self.spell_suggestions = []
            self.message = "SPELL: No errors found | o)ff"
            return
        self.spell_index = (self.spell_index + 1) % len(self.spell_errors)
        self._goto_spell_error()

    def _spell_prev(self) -> None:
        if not self.spell_errors:
            self.spell_suggestions = []
            self.message = "SPELL: No errors found | o)ff"
            return
        self.spell_index = (self.spell_index - 1) % len(self.spell_errors)
        self._goto_spell_error()

    def _goto_spell_error(self) -> None:
        if not (0 <= self.spell_index < len(self.spell_errors)):
            return
        row, start_col, end_col, word = self.spell_errors[self.spell_index]
        self.cursor_row = row
        self.cursor_col = start_col
        suggestions = self.spell_checker.candidates(word)
        self.spell_suggestions = list(suggestions)[:9] if suggestions else []
        self.message = f"[{word}]: suggestions available"

    def _spell_skip(self) -> None:
        if not (0 <= self.spell_index < len(self.spell_errors)):
            return
        word = self.spell_errors[self.spell_index][3]
        self.spell_skip_words.add(word.lower())
        self._scan_spelling()
        if self.spell_index >= len(self.spell_errors):
            self.spell_index = len(self.spell_errors) - 1
        if self.spell_errors:
            self._goto_spell_error()
        else:
            self.message = "SPELL: No more errors | o)ff"

    def _spell_add_to_dict(self) -> None:
        if not (0 <= self.spell_index < len(self.spell_errors)):
            return
        word = self.spell_errors[self.spell_index][3]
        self.spell_checker.word_frequency.load_words([word.lower()])
        self._scan_spelling()
        if self.spell_index >= len(self.spell_errors):
            self.spell_index = len(self.spell_errors) - 1
        if self.spell_errors:
            self._goto_spell_error()
        else:
            self.message = "SPELL: No more errors | o)ff"

    def _spell_replace(self, suggestion_index: int) -> None:
        if not (0 <= self.spell_index < len(self.spell_errors)):
            return
        row, start_col, end_col, word = self.spell_errors[self.spell_index]
        if suggestion_index >= len(self.spell_suggestions):
            self.message = f"No suggestion #{suggestion_index + 1}"
            return

        replacement = self.spell_suggestions[suggestion_index]
        self.buffer.push_undo(UndoType.LINE_CHANGE, row, start_col, row, row)
        line = self.buffer.get_line(row)
        new_line = line[:start_col] + replacement + line[end_col:]
        self.buffer.set_line(row, new_line)
        self.buffer.modified = True
        self._invalidate_syntax_cache()

        self._scan_spelling()
        if self.spell_index >= len(self.spell_errors):
            self.spell_index = len(self.spell_errors) - 1
        if self.spell_errors:
            self._goto_spell_error()
        else:
            self.message = "SPELL: No more errors | o)ff"

    def get_misspelled_cols(self, buf_row: int):
        other_cols = set()
        current_cols = set()
        for i, (row, start_col, end_col, word) in enumerate(self.spell_errors):
            if row == buf_row:
                if i == self.spell_index:
                    current_cols.update(range(start_col, end_col))
                else:
                    other_cols.update(range(start_col, end_col))
        return other_cols, current_cols


class TestPygameSpellCheck:
    """Test spell check in pygame editor."""

    def test_spell_mode_toggle(self):
        """Spell command toggles spell mode."""
        editor = MockPygameEditorWithSpell(["hello world"])
        assert editor.spell_mode == False
        editor._toggle_spell_mode()
        assert editor.spell_mode == True
        editor._toggle_spell_mode()
        assert editor.spell_mode == False

    def test_spell_finds_errors(self):
        """Spell mode finds misspelled words."""
        editor = MockPygameEditorWithSpell(["helo wrold"])
        editor._toggle_spell_mode()
        assert len(editor.spell_errors) == 2  # helo and wrold

    def test_spell_no_errors(self):
        """Spell mode with correct text shows no errors."""
        editor = MockPygameEditorWithSpell(["hello world"])
        editor._toggle_spell_mode()
        assert len(editor.spell_errors) == 0
        assert "No errors" in editor.message

    def test_spell_next_navigation(self):
        """Spell next moves to next error."""
        editor = MockPygameEditorWithSpell(["helo wrold test"])
        editor._toggle_spell_mode()
        assert len(editor.spell_errors) == 2

        editor._spell_next()
        assert editor.spell_index == 0
        assert editor.cursor_col == 0  # 'helo' starts at 0

        editor._spell_next()
        assert editor.spell_index == 1
        assert editor.cursor_col == 5  # 'wrold' starts at 5

    def test_spell_prev_navigation(self):
        """Spell prev moves to previous error."""
        editor = MockPygameEditorWithSpell(["helo wrold"])
        editor._toggle_spell_mode()
        editor.spell_index = 1
        editor._spell_prev()
        assert editor.spell_index == 0

    def test_spell_skip_word(self):
        """Spell skip removes word from errors."""
        editor = MockPygameEditorWithSpell(["helo helo wrold"])
        editor._toggle_spell_mode()
        initial_count = len(editor.spell_errors)
        editor._spell_next()
        editor._spell_skip()
        # Should skip all instances of 'helo'
        assert len(editor.spell_errors) < initial_count
        assert 'helo' in editor.spell_skip_words

    def test_spell_add_to_dict(self):
        """Add to dict removes word from errors."""
        editor = MockPygameEditorWithSpell(["helo wrold"])
        editor._toggle_spell_mode()
        editor._spell_next()
        word = editor.spell_errors[editor.spell_index][3]
        editor._spell_add_to_dict()
        # Word should now be known
        assert word.lower() in editor.spell_checker.known_words

    def test_spell_replace(self):
        """Spell replace changes the word."""
        editor = MockPygameEditorWithSpell(["helo world"])
        editor._toggle_spell_mode()
        editor._spell_next()  # Go to 'helo'
        # Suggestions should include 'hello'
        assert 'hello' in editor.spell_suggestions
        editor._spell_replace(0)  # Replace with first suggestion
        assert editor.buffer.lines[0].startswith("hello")

    def test_spell_get_misspelled_cols(self):
        """get_misspelled_cols returns correct columns."""
        editor = MockPygameEditorWithSpell(["helo world"])
        editor._toggle_spell_mode()
        editor._spell_next()  # Current is 'helo' at cols 0-4
        other, current = editor.get_misspelled_cols(0)
        assert 0 in current
        assert 3 in current
        assert 5 not in current  # 'world' is correct


class MockPygameEditorWithSyntax(MockPygameEditor):
    """Mock editor with syntax highlighting support."""
    def __init__(self, lines=None, filename=None):
        super().__init__(lines)
        self.syntax_highlighter = None
        self.syntax_lang = None
        self.syntax_enabled = True
        if filename:
            self.buffer.filename = filename
            self._setup_syntax_highlighter()

    def _setup_syntax_highlighter(self):
        """Set up syntax highlighter based on filename."""
        try:
            from e3po.syntax.highlighter import SyntaxHighlighter, TREE_SITTER_AVAILABLE
            if not TREE_SITTER_AVAILABLE or not self.buffer.filename:
                return
            self.syntax_highlighter = SyntaxHighlighter()
            self.syntax_lang = self.syntax_highlighter.detect_language(self.buffer.filename)
            if self.syntax_lang:
                self.syntax_highlighter.load_language(self.syntax_lang)
                text = '\n'.join(self.buffer.lines)
                self.syntax_highlighter.parse(text, self.syntax_lang)
        except Exception:
            self.syntax_highlighter = None
            self.syntax_lang = None

    def _get_syntax_colors(self, row: int) -> dict:
        """Get syntax highlighting colors for a row."""
        if not self.syntax_highlighter or not self.syntax_enabled or not self.syntax_lang:
            return {}
        try:
            line = self.buffer.get_line(row)
            highlights = self.syntax_highlighter.get_highlights(row, row, [line])
            colors = {}
            if row in highlights:
                for start_col, end_col, color_idx in highlights[row]:
                    for c in range(start_col, end_col):
                        colors[c] = color_idx
            return colors
        except Exception:
            return {}


class MockPygameEditorWithBookmarks(MockPygameEditor):
    """Mock editor with bookmark support."""
    def __init__(self, lines=None):
        super().__init__(lines)
        self.bookmarks = set()

    def _toggle_bookmark(self):
        """Toggle bookmark on current line."""
        if self.cursor_row in self.bookmarks:
            self.bookmarks.remove(self.cursor_row)
            self.message = f"Bookmark removed (L{self.cursor_row + 1})"
        else:
            self.bookmarks.add(self.cursor_row)
            self.message = f"Bookmark set (L{self.cursor_row + 1})"

    def _prev_bookmark(self):
        """Move to previous bookmark."""
        if not self.bookmarks:
            self.message = "No bookmarks"
            return
        prev_marks = sorted([b for b in self.bookmarks if b < self.cursor_row], reverse=True)
        if prev_marks:
            self.cursor_row = prev_marks[0]
            self.cursor_col = 0
            self.message = f"Bookmark L{self.cursor_row + 1}"
        else:
            self.cursor_row = max(self.bookmarks)
            self.cursor_col = 0
            self.message = f"Bookmark L{self.cursor_row + 1} (wrapped)"

    def _next_bookmark(self):
        """Move to next bookmark."""
        if not self.bookmarks:
            self.message = "No bookmarks"
            return
        next_marks = sorted([b for b in self.bookmarks if b > self.cursor_row])
        if next_marks:
            self.cursor_row = next_marks[0]
            self.cursor_col = 0
            self.message = f"Bookmark L{self.cursor_row + 1}"
        else:
            self.cursor_row = min(self.bookmarks)
            self.cursor_col = 0
            self.message = f"Bookmark L{self.cursor_row + 1} (wrapped)"


class TestPygameBookmarks:
    """Test bookmarks in pygame editor."""

    def test_toggle_bookmark_add(self):
        """Toggle adds bookmark."""
        editor = MockPygameEditorWithBookmarks(["line 1", "line 2", "line 3"])
        editor.cursor_row = 1
        editor._toggle_bookmark()
        assert 1 in editor.bookmarks
        assert "set" in editor.message

    def test_toggle_bookmark_remove(self):
        """Toggle removes existing bookmark."""
        editor = MockPygameEditorWithBookmarks(["line 1", "line 2", "line 3"])
        editor.cursor_row = 1
        editor.bookmarks.add(1)
        editor._toggle_bookmark()
        assert 1 not in editor.bookmarks
        assert "removed" in editor.message

    def test_next_bookmark(self):
        """Next bookmark moves forward."""
        editor = MockPygameEditorWithBookmarks(["line " + str(i) for i in range(10)])
        editor.bookmarks = {2, 5, 8}
        editor.cursor_row = 0
        editor._next_bookmark()
        assert editor.cursor_row == 2

    def test_prev_bookmark(self):
        """Previous bookmark moves backward."""
        editor = MockPygameEditorWithBookmarks(["line " + str(i) for i in range(10)])
        editor.bookmarks = {2, 5, 8}
        editor.cursor_row = 6
        editor._prev_bookmark()
        assert editor.cursor_row == 5

    def test_next_bookmark_wraps(self):
        """Next bookmark wraps to beginning."""
        editor = MockPygameEditorWithBookmarks(["line " + str(i) for i in range(10)])
        editor.bookmarks = {2, 5}
        editor.cursor_row = 7
        editor._next_bookmark()
        assert editor.cursor_row == 2
        assert "wrapped" in editor.message

    def test_prev_bookmark_wraps(self):
        """Previous bookmark wraps to end."""
        editor = MockPygameEditorWithBookmarks(["line " + str(i) for i in range(10)])
        editor.bookmarks = {5, 8}
        editor.cursor_row = 3
        editor._prev_bookmark()
        assert editor.cursor_row == 8
        assert "wrapped" in editor.message

    def test_no_bookmarks(self):
        """No bookmarks shows message."""
        editor = MockPygameEditorWithBookmarks(["line 1"])
        editor._next_bookmark()
        assert "No bookmarks" in editor.message
        editor._prev_bookmark()
        assert "No bookmarks" in editor.message


class TestPygameSyntaxHighlighting:
    """Test syntax highlighting in pygame editor."""

    def test_syntax_detects_python(self):
        """Syntax highlighter detects Python files."""
        editor = MockPygameEditorWithSyntax(["def foo():"], filename="test.py")
        assert editor.syntax_lang == "python"

    def test_syntax_highlights_keywords(self):
        """Syntax highlighting colors Python keywords."""
        editor = MockPygameEditorWithSyntax(["def foo():"], filename="test.py")
        colors = editor._get_syntax_colors(0)
        # 'def' should be colored (keyword)
        assert len(colors) > 0
        # Check that at least some columns are colored
        assert 0 in colors or 1 in colors or 2 in colors

    def test_syntax_no_file_no_colors(self):
        """No filename means no syntax colors."""
        editor = MockPygameEditorWithSyntax(["def foo():"])
        colors = editor._get_syntax_colors(0)
        assert colors == {}

    def test_syntax_disabled(self):
        """Disabled syntax returns no colors."""
        editor = MockPygameEditorWithSyntax(["def foo():"], filename="test.py")
        editor.syntax_enabled = False
        colors = editor._get_syntax_colors(0)
        assert colors == {}

    def test_syntax_unknown_extension(self):
        """Unknown file extension returns no colors."""
        editor = MockPygameEditorWithSyntax(["some text"], filename="test.xyz")
        # Should not crash, just no highlighting
        colors = editor._get_syntax_colors(0)
        assert colors == {} or editor.syntax_lang is None


class MockPygameEditorWithBufferRing:
    """Mock editor for testing buffer ring functionality."""
    def __init__(self):
        self.buffer = Buffer()
        self.buffer.lines = ["line 1"]
        self.buffers = [self.buffer]
        self.current_buffer_idx = 0
        self.cursor_row = 0
        self.cursor_col = 0
        self.top_row = 0
        self.left_col = 0
        self.mark = None
        self.bookmarks = set()
        self.message = ""
        self.running = True
        self.spell_mode = False
        self.spell_errors = []
        self.minimap_dirty = False
        self.syntax_highlighter = None
        self.syntax_lang = None
        self.syntax_cache = {}
        self.syntax_enabled = False

    def _setup_syntax_highlighter(self):
        pass

    def _save_view_to_buffer(self):
        self.buffer.cursor_row = self.cursor_row
        self.buffer.cursor_col = self.cursor_col
        self.buffer.top_row = self.top_row
        self.buffer.left_col = self.left_col
        self.buffer.mark = self.mark

    def _restore_view_from_buffer(self):
        self.cursor_row = self.buffer.cursor_row
        self.cursor_col = self.buffer.cursor_col
        self.top_row = self.buffer.top_row
        self.left_col = self.buffer.left_col
        self.mark = self.buffer.mark
        self.bookmarks = getattr(self.buffer, 'bookmarks', set())

    def _switch_to_buffer(self, idx):
        if len(self.buffers) <= 1:
            self.message = "No other files in ring"
            return
        self._save_view_to_buffer()
        self.buffer.bookmarks = self.bookmarks
        self.current_buffer_idx = idx % len(self.buffers)
        self.buffer = self.buffers[self.current_buffer_idx]
        self._restore_view_from_buffer()
        self._setup_syntax_highlighter()
        self.minimap_dirty = True
        fname = self.buffer.filename or "[No Name]"
        self.message = f"[{self.current_buffer_idx + 1}/{len(self.buffers)}] {fname}"

    def _next_file(self):
        self._switch_to_buffer(self.current_buffer_idx + 1)

    def _prev_file(self):
        self._switch_to_buffer(self.current_buffer_idx - 1)

    def _close_current_buffer(self):
        self.buffer.clear_passphrase()
        if len(self.buffers) == 1:
            self.running = False
        else:
            del self.buffers[self.current_buffer_idx]
            if self.current_buffer_idx >= len(self.buffers):
                self.current_buffer_idx = 0
            self.buffer = self.buffers[self.current_buffer_idx]
            self._restore_view_from_buffer()
            self.message = f"[{self.current_buffer_idx + 1}/{len(self.buffers)}] {self.buffer.filename or '[No Name]'}"


class TestPygameBufferRing:
    """Tests for buffer ring (multiple file editing)."""

    def test_single_buffer_no_switch(self):
        """Can't switch when only one buffer."""
        editor = MockPygameEditorWithBufferRing()
        editor._next_file()
        assert "No other files" in editor.message

    def test_add_buffer_and_switch(self):
        """Add a second buffer and switch to it."""
        editor = MockPygameEditorWithBufferRing()
        buf2 = Buffer()
        buf2.lines = ["file 2"]
        buf2.filename = "file2.txt"
        editor.buffers.append(buf2)

        editor._next_file()
        assert editor.current_buffer_idx == 1
        assert editor.buffer.filename == "file2.txt"
        assert "[2/2]" in editor.message

    def test_next_file_wraps(self):
        """Next file wraps to first buffer."""
        editor = MockPygameEditorWithBufferRing()
        buf2 = Buffer()
        buf2.filename = "file2.txt"
        editor.buffers.append(buf2)

        editor._next_file()  # Go to buffer 2
        editor._next_file()  # Should wrap to buffer 1
        assert editor.current_buffer_idx == 0

    def test_prev_file_wraps(self):
        """Previous file wraps to last buffer."""
        editor = MockPygameEditorWithBufferRing()
        buf2 = Buffer()
        buf2.filename = "file2.txt"
        editor.buffers.append(buf2)

        editor._prev_file()  # Should wrap to buffer 2
        assert editor.current_buffer_idx == 1

    def test_view_state_saved_on_switch(self):
        """View state is saved when switching buffers."""
        editor = MockPygameEditorWithBufferRing()
        buf2 = Buffer()
        editor.buffers.append(buf2)

        # Set cursor position in buffer 1
        editor.cursor_row = 5
        editor.cursor_col = 10

        # Switch to buffer 2
        editor._next_file()

        # Buffer 1 should have saved the position
        assert editor.buffers[0].cursor_row == 5
        assert editor.buffers[0].cursor_col == 10

    def test_view_state_restored_on_switch(self):
        """View state is restored when switching back."""
        editor = MockPygameEditorWithBufferRing()
        buf2 = Buffer()
        buf2.cursor_row = 20
        buf2.cursor_col = 15
        editor.buffers.append(buf2)

        # Switch to buffer 2
        editor._next_file()

        # Position should be restored from buffer 2
        assert editor.cursor_row == 20
        assert editor.cursor_col == 15

    def test_close_buffer_switches_to_next(self):
        """Closing a buffer switches to next one."""
        editor = MockPygameEditorWithBufferRing()
        buf2 = Buffer()
        buf2.filename = "file2.txt"
        editor.buffers.append(buf2)

        # Close first buffer
        editor._close_current_buffer()

        assert len(editor.buffers) == 1
        assert editor.buffer.filename == "file2.txt"
        assert editor.running == True

    def test_close_last_buffer_exits(self):
        """Closing the last buffer exits editor."""
        editor = MockPygameEditorWithBufferRing()
        editor._close_current_buffer()
        assert editor.running == False

    def test_bookmarks_saved_per_buffer(self):
        """Bookmarks are saved per buffer."""
        editor = MockPygameEditorWithBufferRing()
        buf2 = Buffer()
        editor.buffers.append(buf2)

        # Set bookmarks in buffer 1
        editor.bookmarks = {1, 5, 10}

        # Switch to buffer 2
        editor._next_file()

        # Buffer 2 should have no bookmarks
        assert editor.bookmarks == set()

        # Switch back to buffer 1
        editor._prev_file()

        # Bookmarks should be restored
        assert editor.bookmarks == {1, 5, 10}
