import pytest
import math
import os
import subprocess
import tempfile
from fact import fact_it, fact_rec
from show_employee import show_employee
from sum_and_sub import sum_and_sub
from process_list import process_list, process_list_lc, process_list_gen
from my_sum import my_sum
from email_validation import fun
from fibonacci import fibonacci
from average_scores import compute_average_scores
from plane_angle import Point, plane_angle
from complex_numbers import Complex
from circle_square_mk import circle_square_mk
from log_decorator import function_logger
from people_sort import name_format
from phone_number import sort_phone

# ==================== Тестовые данные ====================

test_data = {
    'fact_it': [
        (1, 1),  # граница: минимум n=1
        (2, 2),
        (5, 120),
        (7, 5040),
        (10, 3628800),
    ],
    'fact_rec': [
        (1, 1),  # граница: минимум n=1
        (2, 2),
        (5, 120),
        (7, 5040),
        (10, 3628800),
    ],
    'show_employee': [
        (("Иванов Иван Иванович", 30000), "Иванов Иван Иванович: 30000 ₽"),
        (("Петров Петр Петрович",), "Петров Петр Петрович: 100000 ₽"),
        (("Сидоров Сидор Сидорович", 0), "Сидоров Сидор Сидорович: 0 ₽"),
        (("Test User", 1000000), "Test User: 1000000 ₽"),
        (("", 50000), ": 50000 ₽"),
        (("Test", -1000), "Test: -1000 ₽"),
    ],
    'sum_and_sub': [
        ((5, 3), (8, 2)),
        ((-5, -3), (-8, -2)),
        ((5, -3), (2, 8)),
        ((5.5, 2.5), (8.0, 3.0)),
        ((0, 0), (0, 0)),
        ((1000.5, 500.5), (1501.0, 500.0)),
        ((-10, -5), (-15, -5)),
    ],
    'process_list': [
        ([1, 2, 3, 4], [1, 4, 27, 16]),
        ([2, 4, 6], [4, 16, 36]),
        ([1, 3, 5], [1, 27, 125]),
        ([5], [125]),  # граница: минимум len(arr)=1
    ],
    'my_sum': [
        ((1, 2), 3),
        ((1, 2, 3, 4, 5), 15),
        ((), 0),
        ((1.5, 2.5, 3.0), 7.0),
        ((-1, -2, -3), -6),
        ((5,), 5),
        ((1000, 2000, 3000), 6000),
        ((10, -5, 3, -2), 6),
    ],
    'email_validation': [
        ("test@example.com", True),
        ("test_user@example.com", True),
        ("test-user@example.com", True),
        ("testexample.com", False),
        ("test@exam!ple.com", False),
        ("test@example.comm", False),  # extension > 3
        ("user123@test45.com", True),
        ("user123@test.com", True),
        ("test@site123.com", True),
        ("test@example", False),
        ("@example.com", False),
        ("test@.com", False),
        ("test@example.abc", True),  # граница: максимум extension=3
    ],
    'fibonacci': [
        (1, [0]),  # граница: минимум n=1
        (2, [0, 1]),
        (5, [0, 1, 1, 2, 3]),
        (8, [0, 1, 1, 2, 3, 5, 8, 13]),
        (10, [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]),
    ],
    'complex_add': [
        ((2, 1, 5, 6), (7, 7)),
        ((100, 200, 50, 75), (150, 275)),
    ],
    'complex_sub': [
        ((2, 1, 5, 6), (-3, -5)),
    ],
    'complex_mul': [
        ((2, 1, 5, 6), (4, 17)),
    ],
    'complex_str': [
        ((2, 1), "2.00+1.00i"),
        ((2, -1), "2.00-1.00i"),
        ((2, 0), "2.00+0.00i"),
        ((0, 5), "0.00+5.00i"),
        ((0, 0), "0.00+0.00i"),
    ],
    'point_sub': [
        ((5, 3, 2, 1, 1, 1), (4, 2, 1)),
        ((-1, -2, -3, -4, -5, -6), (3, 3, 3)),
    ],
    'people_sort': [
        ([["Mike", "Thomson", "20", "M"],
          ["Robert", "Downey", "30", "M"],
          ["Alice", "Wonder", "25", "F"]],
         ["Mr. Mike Thomson", "Ms. Alice Wonder", "Mr. Robert Downey"]),
        ([["Anna", "Smith", "18", "F"],
          ["John", "Doe", "18", "M"]],
         ["Ms. Anna Smith", "Mr. John Doe"]),
        ([["Bob", "Brown", "50", "M"]],
         ["Mr. Bob Brown"]),
    ],
    'phone_number': [
        (["89991234567"], ["+7 (999) 123-45-67"]),
        (["79991234567"], ["+7 (999) 123-45-67"]),
        (["9991234567"], ["+7 (999) 123-45-67"]),
        (["+7(999)123-45-67"], ["+7 (999) 123-45-67"]),
        (["89991234567", "84951112233"],
         ["+7 (495) 111-22-33", "+7 (999) 123-45-67"]),  # проверка сортировки
    ],
    'my_sum_argv': [
        (['1', '2'], '3'),
        (['1', '2', '3', '4', '5'], '15'),
        (['10', '20', '30'], '60'),
    ],
}

# Тестовые данные для проверки обработки ошибок
error_test_data = {
    'fact_it': [
        (0, "Error"),   # n < 1
        (-1, "Error"),  # n < 1
        (1000, "Error"),  # n >= 1000
    ],
    'fact_rec': [
        (0, "Error"),   # n < 1
        (-5, "Error"),  # n < 1
        (1000, "Error"),  # n >= 1000
    ],
    'process_list': [
        ([], "Error"),  # len(arr) < 1
        (list(range(1, 1002)), "Error"),  # len(arr) > 1000
    ],
    'process_list_lc': [
        ([], "Error"),  # len(arr) < 1
        (list(range(1, 1002)), "Error"),  # len(arr) > 1000
    ],
    'process_list_gen': [
        ([], ["Error"]),  # len(arr) < 1
        (list(range(1, 1002)), ["Error"]),  # len(arr) > 1000
    ],
    'fibonacci': [
        (0, "Error"),   # n < 1
        (-1, "Error"),  # n < 1
        (16, "Error"),  # n > 15
        (100, "Error"),  # n > 15
    ],
    'average_scores': [
        ([], "Error"),  # X <= 0
        ([tuple([50.0] * 5) for _ in range(101)], "Error"),  # X > 100
        ([tuple([50.0] * 101)], "Error"),  # N > 100
    ],
}

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

@pytest.mark.parametrize("input_data, expected", test_data['fact_it'])
def test_fact_it(input_data, expected):
    assert fact_it(input_data) == expected

@pytest.mark.parametrize("input_data, expected", test_data['fact_rec'])
def test_fact_rec(input_data, expected):
    assert fact_rec(input_data) == expected

@pytest.mark.parametrize("input_data, expected", test_data['show_employee'])
def test_show_employee(input_data, expected):
    assert show_employee(*input_data) == expected

@pytest.mark.parametrize("input_data, expected", test_data['sum_and_sub'])
def test_sum_and_sub(input_data, expected):
    assert sum_and_sub(*input_data) == expected

@pytest.mark.parametrize("input_data, expected", test_data['process_list'])
def test_process_list(input_data, expected):
    assert process_list(input_data) == expected

@pytest.mark.parametrize("input_data, expected", test_data['process_list'])
def test_process_list_lc(input_data, expected):
    assert process_list_lc(input_data) == expected

@pytest.mark.parametrize("input_data, expected", test_data['process_list'])
def test_process_list_gen(input_data, expected):
    assert list(process_list_gen(input_data)) == expected

@pytest.mark.parametrize("input_data, expected", test_data['my_sum'])
def test_my_sum(input_data, expected):
    assert my_sum(*input_data) == expected

@pytest.mark.parametrize("input_data, expected", test_data['email_validation'])
def test_email_validation(input_data, expected):
    assert fun(input_data) == expected

@pytest.mark.parametrize("input_data, expected", test_data['fibonacci'])
def test_fibonacci(input_data, expected):
    assert fibonacci(input_data) == expected

# Тесты для average_scores.py
def test_average_scores():
    scores = [(89, 90, 78, 93, 80), (90, 91, 85, 88, 86), (91, 92, 83, 89, 90.5)]
    result = compute_average_scores(scores)
    assert len(result) == 5
    assert abs(result[0] - 90.0) < 0.1

def test_average_scores_boundary_max():
    scores = [tuple([50.0] * 100) for _ in range(100)]  # граница: максимум X=100, N=100
    result = compute_average_scores(scores)
    assert result != "Error" and len(result) == 100

# Тесты для plane_angle.py

@pytest.mark.parametrize("input_data, expected", test_data['point_sub'])
def test_point_sub(input_data, expected):
    p1 = Point(input_data[0], input_data[1], input_data[2])
    p2 = Point(input_data[3], input_data[4], input_data[5])
    p3 = p1 - p2
    assert (p3.x, p3.y, p3.z) == expected

def test_point_dot():
    p1 = Point(1, 2, 3)
    p2 = Point(4, 5, 6)
    assert p1.dot(p2) == 32

def test_point_cross():
    p1 = Point(1, 0, 0)
    p2 = Point(0, 1, 0)
    p3 = p1.cross(p2)
    assert (p3.x, p3.y, p3.z) == (0, 0, 1)

def test_point_absolute():
    p = Point(3, 4, 0)
    assert p.absolute() == 5.0

def test_point_zero_vector():
    p = Point(0, 0, 0)
    assert p.absolute() == 0.0

# Тесты для complex_numbers.py

@pytest.mark.parametrize("input_data, expected", test_data['complex_add'])
def test_complex_add(input_data, expected):
    c1 = Complex(input_data[0], input_data[1])
    c2 = Complex(input_data[2], input_data[3])
    c3 = c1 + c2
    assert (c3.real, c3.imaginary) == expected

@pytest.mark.parametrize("input_data, expected", test_data['complex_sub'])
def test_complex_sub(input_data, expected):
    c1 = Complex(input_data[0], input_data[1])
    c2 = Complex(input_data[2], input_data[3])
    c3 = c1 - c2
    assert (c3.real, c3.imaginary) == expected

@pytest.mark.parametrize("input_data, expected", test_data['complex_mul'])
def test_complex_mul(input_data, expected):
    c1 = Complex(input_data[0], input_data[1])
    c2 = Complex(input_data[2], input_data[3])
    c3 = c1 * c2
    assert (c3.real, c3.imaginary) == expected

def test_complex_div():
    c1 = Complex(2, 1)
    c2 = Complex(5, 6)
    c3 = c1 / c2
    assert abs(c3.real - 0.26) < 0.01 and abs(c3.imaginary - (-0.11)) < 0.01

def test_complex_mod():
    c = Complex(2, 1)
    m = c.mod()
    assert abs(m.real - 2.236) < 0.01

@pytest.mark.parametrize("input_data, expected", test_data['complex_str'])
def test_complex_str(input_data, expected):
    c = Complex(input_data[0], input_data[1])
    assert str(c) == expected

# Тесты для circle_square_mk.py

def test_circle_square_mk_small():
    result = circle_square_mk(1, 1000)
    expected = math.pi
    assert abs(result - expected) < 1.0

def test_circle_square_mk_large():
    result = circle_square_mk(5, 10000)
    expected = math.pi * 25
    assert abs(result - expected) < 5.0

def test_circle_square_mk_zero_radius():
    result = circle_square_mk(0, 100)
    assert result == 0

def test_circle_square_mk_large_experiments():
    result = circle_square_mk(10, 100000)
    expected = math.pi * 100
    assert abs(result - expected) < 10.0

# Тесты для process_list.py: граничные значения

def test_process_list_boundary_max():
    arr = list(range(1, 1001))  # граница: максимум len(arr)=1000
    result = process_list(arr)
    assert result != "Error" and len(result) == 1000

def test_process_list_lc_boundary_max():
    arr = list(range(1, 1001))  # граница: максимум len(arr)=1000
    result = process_list_lc(arr)
    assert result != "Error" and len(result) == 1000

def test_fibonacci_boundary_max():
    result = fibonacci(15)  # граница: максимум n=15
    assert result != "Error" and len(result) == 15 and result[14] == 377

def test_fact_it_boundary_max():
    result = fact_it(999)  # граница: максимум n=999
    assert result != "Error"

# ==================== Тесты обработки ошибок ====================

@pytest.mark.parametrize("input_data, expected", error_test_data['fact_it'])
def test_fact_it_errors(input_data, expected):
    assert fact_it(input_data) == expected

@pytest.mark.parametrize("input_data, expected", error_test_data['fact_rec'])
def test_fact_rec_errors(input_data, expected):
    assert fact_rec(input_data) == expected

@pytest.mark.parametrize("input_data, expected", error_test_data['process_list'])
def test_process_list_errors(input_data, expected):
    assert process_list(input_data) == expected

@pytest.mark.parametrize("input_data, expected", error_test_data['process_list_lc'])
def test_process_list_lc_errors(input_data, expected):
    assert process_list_lc(input_data) == expected

@pytest.mark.parametrize("input_data, expected", error_test_data['process_list_gen'])
def test_process_list_gen_errors(input_data, expected):
    assert list(process_list_gen(input_data)) == expected

@pytest.mark.parametrize("input_data, expected", error_test_data['fibonacci'])
def test_fibonacci_errors(input_data, expected):
    assert fibonacci(input_data) == expected

@pytest.mark.parametrize("input_data, expected", error_test_data['average_scores'])
def test_average_scores_errors(input_data, expected):
    assert compute_average_scores(input_data) == expected

# ==================== Тесты для people_sort.py ====================

@pytest.mark.parametrize("input_data, expected", test_data['people_sort'])
def test_people_sort(input_data, expected):
    assert name_format(input_data) == expected

# ==================== Тесты для phone_number.py ====================

@pytest.mark.parametrize("input_data, expected", test_data['phone_number'])
def test_phone_number(input_data, expected):
    assert sort_phone(input_data) == expected

# ==================== Тесты для log_decorator.py ====================

def test_log_decorator_writes_file(tmp_path):
    log_file = str(tmp_path / "test.log")

    @function_logger(log_file)
    def add(a, b):
        return a + b

    result = add(2, 3)
    assert result == 5
    assert os.path.exists(log_file)
    with open(log_file, 'r', encoding='utf-8') as f:
        content = f.read()
    assert 'add' in content
    assert '(2, 3)' in content
    assert '5' in content

def test_log_decorator_no_return(tmp_path):
    log_file = str(tmp_path / "test.log")

    @function_logger(log_file)
    def noop():
        pass

    result = noop()
    assert result is None
    with open(log_file, 'r', encoding='utf-8') as f:
        content = f.read()
    assert 'noop' in content
    assert '-' in content  # None записывается как '-'

def test_log_decorator_kwargs(tmp_path):
    log_file = str(tmp_path / "test.log")

    @function_logger(log_file)
    def greet(name="World"):
        return f"Hello, {name}!"

    result = greet(name="Test")
    assert result == "Hello, Test!"
    with open(log_file, 'r', encoding='utf-8') as f:
        content = f.read()
    assert 'greet' in content
    assert "name" in content

def test_log_decorator_multiple_calls(tmp_path):
    log_file = str(tmp_path / "test.log")

    @function_logger(log_file)
    def square(x):
        return x * x

    square(3)
    square(5)
    with open(log_file, 'r', encoding='utf-8') as f:
        content = f.read()
    assert content.count('square') == 2

# ==================== Тесты для my_sum_argv.py (subprocess) ====================

INTERPRETER = 'python'

def run_script(filename, args=None):
    cmd = [INTERPRETER, filename] + (args if args else [])
    proc = subprocess.run(cmd, capture_output=True, text=True, check=False)
    return proc.stdout.strip()

@pytest.mark.parametrize("input_data, expected", test_data['my_sum_argv'])
def test_my_sum_argv(input_data, expected):
    assert run_script('my_sum_argv.py', input_data) == expected

# ==================== Тесты для file_search.py (subprocess) ====================

def test_file_search_found(tmp_path):
    test_file = tmp_path / "hello.txt"
    test_file.write_text("line1\nline2\nline3\n", encoding='utf-8')
    result = subprocess.run(
        [INTERPRETER, 'file_search.py', 'hello.txt'],
        capture_output=True, text=True, check=False, cwd=str(tmp_path)
    )

    import shutil
    shutil.copy('file_search.py', str(tmp_path / 'file_search.py'))
    result = subprocess.run(
        [INTERPRETER, 'file_search.py', 'hello.txt'],
        capture_output=True, text=True, check=False, cwd=str(tmp_path)
    )
    assert 'line1' in result.stdout

def test_file_search_not_found(tmp_path):
    import shutil
    shutil.copy('file_search.py', str(tmp_path / 'file_search.py'))
    result = subprocess.run(
        [INTERPRETER, 'file_search.py', 'nonexistent.txt'],
        capture_output=True, text=True, check=False, cwd=str(tmp_path)
    )
    assert 'не найден' in result.stdout

def test_file_search_max_5_lines(tmp_path):
    import shutil
    shutil.copy('file_search.py', str(tmp_path / 'file_search.py'))
    test_file = tmp_path / "many_lines.txt"
    test_file.write_text('\n'.join([f"line{i}" for i in range(10)]), encoding='utf-8')
    result = subprocess.run(
        [INTERPRETER, 'file_search.py', 'many_lines.txt'],
        capture_output=True, text=True, check=False, cwd=str(tmp_path)
    )
    lines = result.stdout.strip().split('\n')
    assert len(lines) == 5  # выводит только первые 5 строк

# ==================== Тесты для files_sort.py (subprocess) ====================

def test_files_sort(tmp_path):
    import shutil
    test_dir = tmp_path / "data"
    test_dir.mkdir()
    (test_dir / "b.txt").write_text("", encoding='utf-8')
    (test_dir / "a.txt").write_text("", encoding='utf-8')
    (test_dir / "c.py").write_text("", encoding='utf-8')
    shutil.copy('files_sort.py', str(tmp_path / 'files_sort.py'))
    result = subprocess.run(
        [INTERPRETER, 'files_sort.py', str(test_dir)],
        capture_output=True, text=True, check=False, cwd=str(tmp_path)
    )
    lines = result.stdout.strip().split('\n')
    # Сортировка: сначала по расширению, потом по имени
    assert lines == ['c.py', 'a.txt', 'b.txt']

def test_files_sort_no_extension(tmp_path):
    import shutil
    test_dir = tmp_path / "data"
    test_dir.mkdir()
    (test_dir / "Makefile").write_text("", encoding='utf-8')
    (test_dir / "a.txt").write_text("", encoding='utf-8')
    shutil.copy('files_sort.py', str(tmp_path / 'files_sort.py'))
    result = subprocess.run(
        [INTERPRETER, 'files_sort.py', str(test_dir)],
        capture_output=True, text=True, check=False, cwd=str(tmp_path)
    )
    lines = result.stdout.strip().split('\n')
    # Файлы без расширения (ext='') идут первыми по sorted
    assert lines[0] == 'Makefile'
