#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Тест корректности сортировки для sortucs4 (Pascal/C версии)
Проверяет правильность сортировки UTF-8 строк и таблиц
"""

import os
import sys
import subprocess
import tempfile
import shutil
import random
import string
from pathlib import Path

# ============================================================================
# Конфигурация
# ============================================================================
SORTER_PASCAL = "/usr/bin/sortucs4"  # Путь к Pascal версии
SORTER_C = "./sortucs4"      # Путь к C версии (измените при необходимости)

# Цветной вывод для терминала
class Colors:
    GREEN = '\033[92m'
    RED = '\033[91m'
    YELLOW = '\033[93m'
    BLUE = '\033[94m'
    RESET = '\033[0m'
    BOLD = '\033[1m'

def print_success(msg):
    print(f"{Colors.GREEN}✓ {msg}{Colors.RESET}")

def print_error(msg):
    print(f"{Colors.RED}✗ {msg}{Colors.RESET}")

def print_info(msg):
    print(f"{Colors.BLUE}ℹ {msg}{Colors.RESET}")

def print_test_header(name):
    print(f"\n{Colors.BOLD}{Colors.YELLOW}▶ {name}{Colors.RESET}")

# ============================================================================
# Генераторы тестовых данных
# ============================================================================

def generate_unicode_strings(count=100):
    """Генерация разнообразных Unicode строк"""
    strings = []
    
    # Базовые латинские строки
    for i in range(count // 4):
        strings.append(f"string_{i:05d}")
        strings.append(f"TEST_{i:03d}")
        strings.append(f"abc_{chr(65 + i % 26)}_{i}")
        strings.append(f"z_{i}_abc")
    
    # Русские строки
    rus_words = ["привет", "мир", "солнце", "дерево", "машина", "змея", "яблоко", "ёжик", "юла", "якорь"]
    for i in range(min(count // 4, len(rus_words) * 3)):
        strings.append(f"{rus_words[i % len(rus_words)]}_{i}")
        strings.append(f"Ещё_строка_{i}")
        strings.append(f"Ёлка_на_горе_{i}")
    
    # Строки с диакритикой
    diacritic_strings = [
        "café", "résumé", "façade", "élève", "naïve", "coöperate",
        "François", "Müller", "Schön", "Gödel", "Łódź", "Żółć"
    ]
    strings.extend(diacritic_strings)
    
    # Строки с разными языками
    multi_lang = [
        "日本語のテキスト", "中文文本", "한글 텍스트", "العربية", "עברית",
        "Ελληνικά", "русский", "हिन्दी", "ไทย", "አማርኛ"
    ]
    strings.extend(multi_lang)
    
    # Строки с эмодзи и спецсимволами
    special = [
        "😀😃😄😁", "❤️💙💚💛", "★☆✪✫", "§¶†‡", "∞≈≠≤≥",
        "∑∏∫√", "←↑→↓↔", "☀☁☂☃", "♠♣♥♦", "♪♫♬"
    ]
    strings.extend(special)
    
    # Пустые строки и строки с пробелами
    strings.append("")
    strings.append("   ")
    strings.append("\t")
    strings.append(" \t \n ")
    
    # Случайные строки
    chars = list(string.ascii_letters + string.digits + " _-!@#$%^&*()")
    for i in range(count // 2):
        length = random.randint(1, 50)
        rand_str = ''.join(random.choice(chars) for _ in range(length))
        strings.append(rand_str)
    
    # Уникализируем и перемешиваем
    strings = list(set(strings))
    random.shuffle(strings)
    
    return strings[:count]

def generate_table_data(rows=50, cols=5):
    """Генерация табличных данных"""
    headers = [f"col_{i}" for i in range(cols)]
    data = [headers]
    
    # Числовые значения
    numbers = list(range(1, rows))
    random.shuffle(numbers)
    
    # Строковые значения
    strings = generate_unicode_strings(rows * 2)
    random.shuffle(strings)
    
    for row_num in range(rows - 1):
        row = []
        for col in range(cols):
            if col == 0:
                # Первый столбец - числа
                row.append(str(numbers[row_num] if row_num < len(numbers) else row_num))
            elif col == 1:
                # Второй столбец - строки
                row.append(strings[row_num % len(strings)])
            elif col == 2:
                # Третий столбец - даты
                row.append(f"2024-{random.randint(1,12):02d}-{random.randint(1,28):02d}")
            else:
                # Остальные - случайные данные
                row.append(f"data_{random.randint(1,1000)}_{row_num}")
        data.append(row)
    
    return data

# ============================================================================
# Функции сравнения для верификации
# ============================================================================

def sort_key_unicode(s):
    """Ключ для сортировки на уровне Unicode code points"""
    return [ord(c) for c in s]

def sort_table_by_column(table, col_idx, delimiter='\t'):
    """Сортировка таблицы по колонке (эталонная реализация)"""
    headers = table[0] if table else []
    data_rows = table[1:] if table else []
    
    # Сохраняем заголовки отдельно, сортируем только данные
    sorted_data = sorted(data_rows, key=lambda row: (row[col_idx] if col_idx < len(row) else ''))
    
    return [headers] + sorted_data

def verify_sorting(original_lines, sorted_lines):
    """Проверка корректности сортировки"""
    # Сортируем оригинал эталонным способом
    expected = sorted(original_lines, key=sort_key_unicode)
    
    if len(sorted_lines) != len(expected):
        return False, f"Размер не совпадает: {len(sorted_lines)} vs {len(expected)}"
    
    # Поэлементное сравнение
    for i, (got, exp) in enumerate(zip(sorted_lines, expected)):
        if got != exp:
            # Показываем контекст ошибки
            context_start = max(0, i - 3)
            context_end = min(len(sorted_lines), i + 4)
            error_msg = f"\n  Несовпадение на позиции {i}:\n"
            error_msg += f"    Ожидалось: {repr(exp)}\n"
            error_msg += f"    Получено:  {repr(got)}\n"
            error_msg += "  Контекст:\n"
            for j in range(context_start, context_end):
                marker = ">>>" if j == i else "   "
                error_msg += f"    {marker} {repr(sorted_lines[j])}\n"
            return False, error_msg
    
    return True, "OK"

def verify_table_sorting(original_table, sorted_table, col_idx, delimiter='\t'):
    """Проверка корректности сортировки таблицы"""
    # Сохраняем заголовки и данные
    original_headers = original_table[0] if original_table else []
    original_data = original_table[1:] if original_table else []
    
    expected_headers = original_headers
    expected_data = sorted(original_data, key=lambda row: (row[col_idx] if col_idx < len(row) else ''))
    
    if len(sorted_table) != len(original_table):
        return False, f"Размер не совпадает: {len(sorted_table)} vs {len(original_table)}"
    
    # Проверяем заголовки
    if sorted_table[0] != expected_headers:
        return False, f"Заголовки не совпадают: {sorted_table[0]} vs {expected_headers}"
    
    # Проверяем данные
    for i, (got, exp) in enumerate(zip(sorted_table[1:], expected_data)):
        if got != exp:
            return False, f"Несовпадение в строке {i+1}: {got} vs {exp}"
    
    return True, "OK"

# ============================================================================
# Сравнение Pascal и C версий
# ============================================================================

def compare_pascal_c(pascal_result, c_result):
    """Сравнение результатов Pascal и C версий"""
    if len(pascal_result) != len(c_result):
        return False, f"Разное количество строк: {len(pascal_result)} vs {len(c_result)}"
    
    differences = []
    for i, (p, c) in enumerate(zip(pascal_result, c_result)):
        if p != c:
            differences.append((i, p, c))
            if len(differences) >= 5:  # Показываем не более 5 отличий
                differences.append((None, "...", "..."))
                break
    
    if differences:
        msg = f"Найдено {len(differences)} различий:\n"
        for diff in differences[:5]:
            if diff[0] is None:
                msg += "    ...\n"
            else:
                msg += f"    Строка {diff[0]}: Pascal={repr(diff[1])} vs C={repr(diff[2])}\n"
        return False, msg
    
    return True, "OK"

# ============================================================================
# Основные тесты
# ============================================================================

class SortTester:
    def __init__(self, sorter_path):
        self.sorter_path = sorter_path
        self.temp_dir = None
        self.passed = 0
        self.failed = 0
        self.tests = []
    
    def setup(self):
        """Создание временной директории"""
        self.temp_dir = tempfile.mkdtemp(prefix="sortucs4_test_")
        return self.temp_dir
    
    def teardown(self):
        """Удаление временной директории"""
        if self.temp_dir and os.path.exists(self.temp_dir):
            shutil.rmtree(self.temp_dir)
    
    def run_test(self, name, test_func):
        """Запуск отдельного теста"""
        print_test_header(name)
        try:
            result = test_func()
            if result:
                self.passed += 1
                print_success(f"{name} - ПРОЙДЕН")
                return True
            else:
                self.failed += 1
                print_error(f"{name} - ПРОВАЛЕН")
                return False
        except Exception as e:
            self.failed += 1
            print_error(f"{name} - ОШИБКА: {e}")
            return False
    
    def test_simple_strings(self):
        """Тест 1: Простые латинские строки"""
        lines = ["banana", "apple", "cherry", "date", "elderberry"]
        expected = sorted(lines)
        
        file_path = os.path.join(self.temp_dir, "simple.txt")
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write('\n'.join(lines))
        
        result = subprocess.run([self.sorter_path, file_path], 
                                capture_output=True, text=True)
        
        with open(file_path, 'r', encoding='utf-8') as f:
            sorted_lines = [line.rstrip('\n') for line in f.readlines()]
        
        return sorted_lines == expected
    
    def test_unicode_strings(self):
        """Тест 2: Unicode строки (русские, диакритика, эмодзи)"""
        strings = generate_unicode_strings(100)
        expected = sorted(strings, key=sort_key_unicode)
        
        file_path = os.path.join(self.temp_dir, "unicode.txt")
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write('\n'.join(strings))
        
        subprocess.run([self.sorter_path, file_path], capture_output=True, text=True)
        
        with open(file_path, 'r', encoding='utf-8') as f:
            sorted_lines = [line.rstrip('\n') for line in f.readlines()]
        
        return sorted_lines == expected
    
    def test_empty_lines(self):
        """Тест 3: Пустые строки и строки с пробелами"""
        lines = ["", "   ", "", "\t", "text", "   spaces   ", "", "last"]
        expected = sorted(lines, key=sort_key_unicode)
        
        file_path = os.path.join(self.temp_dir, "empty.txt")
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write('\n'.join(lines))
        
        subprocess.run([self.sorter_path, file_path], capture_output=True, text=True)
        
        with open(file_path, 'r', encoding='utf-8') as f:
            sorted_lines = [line.rstrip('\n') for line in f.readlines()]
        
        return sorted_lines == expected
    
    def test_single_line(self):
        """Тест 4: Файл с одной строкой"""
        line = "single line of text"
        
        file_path = os.path.join(self.temp_dir, "single.txt")
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write(line)
        
        result = subprocess.run([self.sorter_path, file_path], capture_output=True, text=True)
        
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read().strip()
        
        return content == line and result.returncode == 0
    
    def test_already_sorted(self):
        """Тест 5: Уже отсортированный файл"""
        lines = ["apple", "banana", "cherry", "date", "elderberry"]
        expected = lines.copy()
        
        file_path = os.path.join(self.temp_dir, "sorted.txt")
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write('\n'.join(lines))
        
        subprocess.run([self.sorter_path, file_path], capture_output=True, text=True)
        
        with open(file_path, 'r', encoding='utf-8') as f:
            sorted_lines = [line.rstrip('\n') for line in f.readlines()]
        
        return sorted_lines == expected
    
    def test_reverse_sorted(self):
        """Тест 6: Обратно отсортированный файл"""
        lines = ["elderberry", "date", "cherry", "banana", "apple"]
        expected = sorted(lines)
        
        file_path = os.path.join(self.temp_dir, "reverse.txt")
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write('\n'.join(lines))
        
        subprocess.run([self.sorter_path, file_path], capture_output=True, text=True)
        
        with open(file_path, 'r', encoding='utf-8') as f:
            sorted_lines = [line.rstrip('\n') for line in f.readlines()]
        
        return sorted_lines == expected
    
    def test_table_tab_separated(self):
        """Тест 7: Таблица с табуляцией как разделителем"""
        table_data = generate_table_data(30, 4)
        col_to_sort = 1  # Второй столбец
        
        # Сохраняем как TSV
        file_path = os.path.join(self.temp_dir, "table.tsv")
        with open(file_path, 'w', encoding='utf-8') as f:
            for row in table_data:
                f.write('\t'.join(row) + '\n')
        
        # Сортируем по второму столбцу (индекс 1 в 0-based)
        subprocess.run([self.sorter_path, file_path, str(col_to_sort + 1)], 
                      capture_output=True, text=True)
        
        # Читаем результат
        with open(file_path, 'r', encoding='utf-8') as f:
            sorted_table = [line.rstrip('\n').split('\t') for line in f.readlines()]
        
        # Проверяем
        expected_table = sort_table_by_column(table_data, col_to_sort, '\t')
        
        return sorted_table == expected_table
    
    def test_table_custom_delimiter(self):
        """Тест 8: Таблица с пользовательским разделителем"""
        delimiter = '|'
        table_data = generate_table_data(30, 3)
        col_to_sort = 2  # Третий столбец
        
        # Сохраняем с разделителем |
        file_path = os.path.join(self.temp_dir, "table_pipe.txt")
        with open(file_path, 'w', encoding='utf-8') as f:
            for row in table_data:
                f.write(delimiter.join(row) + '\n')
        
        # Сортируем
        subprocess.run([self.sorter_path, file_path, str(col_to_sort + 1), delimiter], 
                      capture_output=True, text=True)
        
        # Читаем результат
        with open(file_path, 'r', encoding='utf-8') as f:
            sorted_table = [line.rstrip('\n').split(delimiter) for line in f.readlines()]
        
        # Проверяем
        expected_table = sort_table_by_column(table_data, col_to_sort, delimiter)
        
        return sorted_table == expected_table
    
    def test_large_file(self):
        """Тест 9: Большой файл (10000 строк)"""
        strings = generate_unicode_strings(5000) * 2
        random.shuffle(strings)
        expected = sorted(strings, key=sort_key_unicode)
        
        file_path = os.path.join(self.temp_dir, "large.txt")
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write('\n'.join(strings))
        
        result = subprocess.run([self.sorter_path, file_path], 
                                capture_output=True, text=True)
        
        with open(file_path, 'r', encoding='utf-8') as f:
            sorted_lines = [line.rstrip('\n') for line in f.readlines()]
        
        return sorted_lines == expected
    
    def test_case_sensitivity(self):
        """Тест 10: Чувствительность к регистру (должна быть, как в Pascal версии)"""
        lines = ["Zebra", "apple", "Banana", "cherry", "Apple"]
        # В Pascal версии сравнение по Unicode code points, поэтому заглавные буквы идут перед строчными
        expected = sorted(lines, key=sort_key_unicode)
        
        file_path = os.path.join(self.temp_dir, "case.txt")
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write('\n'.join(lines))
        
        subprocess.run([self.sorter_path, file_path], capture_output=True, text=True)
        
        with open(file_path, 'r', encoding='utf-8') as f:
            sorted_lines = [line.rstrip('\n') for line in f.readlines()]
        
        return sorted_lines == expected
    
    def test_numeric_prefix(self):
        """Тест 11: Строки с числовыми префиксами"""
        lines = ["10_text", "2_text", "1_text", "20_text", "3_text"]
        expected = sorted(lines, key=sort_key_unicode)
        
        file_path = os.path.join(self.temp_dir, "numeric.txt")
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write('\n'.join(lines))
        
        subprocess.run([self.sorter_path, file_path], capture_output=True, text=True)
        
        with open(file_path, 'r', encoding='utf-8') as f:
            sorted_lines = [line.rstrip('\n') for line in f.readlines()]
        
        return sorted_lines == expected
    
    def test_special_chars_first(self):
        """Тест 12: Специальные символы в начале строк"""
        lines = ["_underscore", "-dash", ".dot", "normal", "!exclamation", "@at"]
        expected = sorted(lines, key=sort_key_unicode)
        
        file_path = os.path.join(self.temp_dir, "special.txt")
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write('\n'.join(lines))
        
        subprocess.run([self.sorter_path, file_path], capture_output=True, text=True)
        
        with open(file_path, 'r', encoding='utf-8') as f:
            sorted_lines = [line.rstrip('\n') for line in f.readlines()]
        
        return sorted_lines == expected

def compare_versions():
    """Сравнение результатов Pascal и C версий"""
    print_test_header("Сравнение Pascal и C версий")
    
    tester_pascal = SortTester(SORTER_PASCAL)
    tester_c = SortTester(SORTER_C)
    
    all_match = True
    differences = []
    
    # Проверяем на разных наборах данных
    test_cases = [
        ("Простые строки", ["banana", "apple", "cherry", "date"]),
        ("Unicode", generate_unicode_strings(50)),
        ("Пустые строки", ["", "   ", "", "text", "", "last"]),
        ("Регистр", ["Zebra", "apple", "Banana", "cherry"]),
        ("Числовые префиксы", ["10_text", "2_text", "1_text", "20_text"]),
    ]
    
    for test_name, lines in test_cases:
        # Создаем временные файлы для обеих версий
        file_pascal = os.path.join(tester_pascal.setup(), "test.txt")
        file_c = os.path.join(tester_c.setup(), "test.txt")
        
        with open(file_pascal, 'w', encoding='utf-8') as f:
            f.write('\n'.join(lines))
        with open(file_c, 'w', encoding='utf-8') as f:
            f.write('\n'.join(lines))
        
        # Запускаем обе версии
        subprocess.run([SORTER_PASCAL, file_pascal], capture_output=True, text=True)
        subprocess.run([SORTER_C, file_c], capture_output=True, text=True)
        
        # Читаем результаты
        with open(file_pascal, 'r', encoding='utf-8') as f:
            pascal_result = [l.rstrip('\n') for l in f.readlines()]
        with open(file_c, 'r', encoding='utf-8') as f:
            c_result = [l.rstrip('\n') for l in f.readlines()]
        
        if pascal_result != c_result:
            all_match = False
            differences.append((test_name, pascal_result, c_result))
        
        tester_pascal.teardown()
        tester_c.teardown()
    
    if all_match:
        print_success("Pascal и C версии дают идентичные результаты")
    else:
        print_error(f"Найдены различия в {len(differences)} тестах")
        for test_name, p_res, c_res in differences:
            print(f"  {test_name}:")
            print(f"    Pascal: {p_res[:5]}...")
            print(f"    C:      {c_res[:5]}...")
    
    return all_match

# ============================================================================
# Запуск всех тестов
# ============================================================================

def run_all_tests(sorter_path):
    """Запуск всех тестов для указанного сортировщика"""
    print(f"\n{Colors.BOLD}{Colors.BLUE}Testing: {sorter_path}{Colors.RESET}")
    
    if not os.path.exists(sorter_path):
        print_error(f"Сортировщик не найден: {sorter_path}")
        return None
    
    tester = SortTester(sorter_path)
    
    # Список всех тестов
    tests = [
        ("Простые латинские строки", tester.test_simple_strings),
        ("Unicode строки", tester.test_unicode_strings),
        ("Пустые строки и пробелы", tester.test_empty_lines),
        ("Файл с одной строкой", tester.test_single_line),
        ("Уже отсортированный файл", tester.test_already_sorted),
        ("Обратно отсортированный файл", tester.test_reverse_sorted),
        ("Таблица (табуляция)", tester.test_table_tab_separated),
        ("Таблица (пользовательский разделитель)", tester.test_table_custom_delimiter),
        ("Большой файл (10000 строк)", tester.test_large_file),
        ("Чувствительность к регистру", tester.test_case_sensitivity),
        ("Числовые префиксы", tester.test_numeric_prefix),
        ("Специальные символы", tester.test_special_chars_first),
    ]
    
    tester.setup()
    
    for name, test_func in tests:
        tester.run_test(name, test_func)
    
    tester.teardown()
    
    # Вывод итогов
    print(f"\n{Colors.BOLD}Результаты для {sorter_path}:{Colors.RESET}")
    print(f"  Пройдено: {Colors.GREEN}{tester.passed}{Colors.RESET}")
    print(f"  Провалено: {Colors.RED}{tester.failed}{Colors.RESET}")
    print(f"  Всего: {tester.passed + tester.failed}")
    
    return tester.passed, tester.failed

# ============================================================================
# Main
# ============================================================================

def main():
    print(f"{Colors.BOLD}{Colors.BLUE}{'='*60}{Colors.RESET}")
    print(f"{Colors.BOLD}Тест корректности сортировки sortucs4{Colors.RESET}")
    print(f"{Colors.BLUE}{'='*60}{Colors.RESET}\n")
    
    # Проверяем наличие сортировщиков
    results = {}
    
    if os.path.exists(SORTER_PASCAL):
        pascal_result = run_all_tests(SORTER_PASCAL)
        if pascal_result:
            results['Pascal'] = pascal_result
    else:
        print_info(f"Pascal версия не найдена по пути: {SORTER_PASCAL}")
    
    if os.path.exists(SORTER_C):
        c_result = run_all_tests(SORTER_C)
        if c_result:
            results['C'] = c_result
    else:
        print_info(f"C версия не найдена по пути: {SORTER_C}")
    
    # Сравниваем версии, если обе доступны
    if 'Pascal' in results and 'C' in results:
        print(f"\n{Colors.BOLD}{Colors.BLUE}{'='*60}{Colors.RESET}")
        compare_versions()
    
    # Итоговый отчет
    print(f"\n{Colors.BOLD}{Colors.BLUE}{'='*60}{Colors.RESET}")
    print(f"{Colors.BOLD}ИТОГОВЫЙ ОТЧЕТ{Colors.RESET}")
    print(f"{Colors.BLUE}{'='*60}{Colors.RESET}")
    
    for name, (passed, failed) in results.items():
        total = passed + failed
        percent = (passed / total * 100) if total > 0 else 0
        status = f"{Colors.GREEN}✓{Colors.RESET}" if failed == 0 else f"{Colors.RED}✗{Colors.RESET}"
        print(f"{status} {name}: {passed}/{total} тестов пройдено ({percent:.1f}%)")
    
    if not results:
        print_error("Не найдено ни одного сортировщика для тестирования")

if __name__ == "__main__":
    main()