"""
Comprehensive tests for @gatto decorator.
"""

import csv
import json
import time
from pathlib import Path

import pytest

from gattopy.decorators import gatto


class TestGattoBasicFunctionality:
    """Test basic @gatto decorator functionality."""

    def test_basic_decoration_no_params(self):
        """Test @gatto without any parameters."""

        @gatto
        def simple_function():
            return 42

        result = simple_function()
        assert result == 42
        assert hasattr(simple_function, "last_stats")
        assert hasattr(simple_function, "last_profile")

    def test_basic_decoration_with_parentheses(self):
        """Test @gatto() with parentheses."""

        @gatto()
        def simple_function():
            return "test"

        result = simple_function()
        assert result == "test"

    def test_function_name_preserved(self):
        """Test that original function name is preserved."""

        @gatto(silent=True)
        def my_custom_function():
            return 100

        assert my_custom_function.__name__ == "my_custom_function"
        my_custom_function()
        assert my_custom_function.last_stats["function_name"] == "my_custom_function"

    def test_function_with_arguments(self):
        """Test @gatto on function with arguments."""

        @gatto(silent=True)
        def add_numbers(a, b, c=0):
            return a + b + c

        result = add_numbers(5, 3)
        assert result == 8

        result = add_numbers(5, 3, c=10)
        assert result == 18

    def test_function_with_kwargs(self):
        """Test function with keyword arguments."""

        @gatto(silent=True)
        def greet(name, greeting="Hello"):
            return f"{greeting}, {name}!"

        result = greet("Alice")
        assert result == "Hello, Alice!"

        result = greet("Bob", greeting="Hi")
        assert result == "Hi, Bob!"

    def test_return_value_types(self):
        """Test various return value types are preserved."""

        @gatto(silent=True)
        def return_list():
            return [1, 2, 3]

        @gatto(silent=True)
        def return_dict():
            return {"key": "value"}

        @gatto(silent=True)
        def return_none():
            return None

        assert return_list() == [1, 2, 3]
        assert return_dict() == {"key": "value"}
        assert return_none() is None


class TestGattoReturnDataParameter:
    """Test return_data parameter behavior."""

    def test_return_data_false_default(self):
        """Test that return_data=False is the default."""

        @gatto(silent=True)
        def simple():
            return 42

        result = simple()
        assert result == 42
        assert not isinstance(result, tuple)

    def test_return_data_true(self):
        """Test return_data=True returns tuple."""

        @gatto(silent=True, return_data=True)
        def simple():
            return 42

        result, stats = simple()
        assert result == 42
        assert isinstance(stats, dict)
        assert "execution_time" in stats
        assert "function_name" in stats

    def test_return_data_tuple_unpacking(self):
        """Test tuple unpacking with return_data=True."""

        @gatto(silent=True, return_data=True)
        def calculate(x, y):
            return x * y

        value, stats = calculate(5, 10)
        assert value == 50
        assert stats["function_name"] == "calculate"

    def test_stats_structure(self):
        """Test the structure of returned stats."""

        @gatto(silent=True, return_data=True)
        def test_func(n):
            return sum(range(n))

        result, stats = test_func(100)

        assert "function_name" in stats
        assert "execution_time" in stats
        assert "execution_time_formatted" in stats
        assert "memory_peak_mb" in stats
        assert "memory_current_mb" in stats
        assert "arguments" in stats
        assert stats["arguments"]["n"] == "100"


class TestGattoSilentParameter:
    """Test silent parameter behavior."""

    def test_silent_true_no_output(self, capsys):
        """Test silent=True suppresses output."""

        @gatto(silent=True)
        def quiet_function():
            return "result"

        quiet_function()
        captured = capsys.readouterr()
        assert captured.out == ""

    def test_silent_false_produces_output(self, capsys):
        """Test silent=False produces console output."""

        @gatto(silent=False)
        def loud_function():
            return "result"

        loud_function()
        captured = capsys.readouterr()
        assert "loud_function" in captured.out
        assert (
            "Execution Time" in captured.out or "execution_time" in captured.out.lower()
        )

    def test_silent_default_is_false(self, capsys):
        """Test that silent defaults to False."""

        @gatto()
        def default_function():
            return 1

        default_function()
        captured = capsys.readouterr()
        assert len(captured.out) > 0  # Should have output


class TestGattoAccumulation:
    """Test automatic accumulation functionality."""

    def test_accumulate_default_true(self):
        """Test that accumulate=True is the default."""

        @gatto(silent=True)
        def counter():
            return 1

        counter()
        counter()
        counter()

        assert counter.call_count() == 3
        assert len(counter.get_stats()) == 3

    def test_accumulate_false(self):
        """Test accumulate=False doesn't store stats."""

        @gatto(silent=True, accumulate=False)
        def no_accumulation():
            return 1

        no_accumulation()
        no_accumulation()
        no_accumulation()

        assert no_accumulation.call_count() == 0
        assert len(no_accumulation.get_stats()) == 0

    def test_accumulation_order(self):
        """Test that accumulation preserves call order."""

        @gatto(silent=True)
        def numbered_calls(n):
            return n * 2

        numbered_calls(1)
        numbered_calls(2)
        numbered_calls(3)

        stats = numbered_calls.get_stats()
        assert len(stats) == 3
        assert stats[0]["arguments"]["n"] == "1"
        assert stats[1]["arguments"]["n"] == "2"
        assert stats[2]["arguments"]["n"] == "3"

    def test_multiple_functions_independent_accumulation(self):
        """Test that different decorated functions have independent accumulation."""

        @gatto(silent=True)
        def func_a():
            return "a"

        @gatto(silent=True)
        def func_b():
            return "b"

        func_a()
        func_a()
        func_b()

        assert func_a.call_count() == 2
        assert func_b.call_count() == 1


class TestGattoMethods:
    """Test methods attached to decorated functions."""

    def test_call_count_method(self):
        """Test call_count() method."""

        @gatto(silent=True)
        def counter():
            return 1

        assert counter.call_count() == 0

        counter()
        assert counter.call_count() == 1

        for _ in range(9):
            counter()
        assert counter.call_count() == 10

    def test_get_stats_method(self):
        """Test get_stats() returns list of dicts."""

        @gatto(silent=True)
        def test_func(x):
            return x**2

        test_func(2)
        test_func(3)

        stats = test_func.get_stats()
        assert isinstance(stats, list)
        assert len(stats) == 2
        assert all(isinstance(s, dict) for s in stats)

    def test_get_profiles_method(self):
        """Test get_profiles() returns ProfileResult objects."""

        @gatto(silent=True)
        def test_func():
            return 1

        test_func()
        test_func()

        profiles = test_func.get_profiles()
        assert isinstance(profiles, list)
        assert len(profiles) == 2

        from gattopy.core import ProfileResult

        assert all(isinstance(p, ProfileResult) for p in profiles)

    def test_clear_stats_method(self):
        """Test clear_stats() clears accumulated data."""

        @gatto(silent=True)
        def test_func():
            return 1

        test_func()
        test_func()
        test_func()
        assert test_func.call_count() == 3

        test_func.clear_stats()
        assert test_func.call_count() == 0
        assert len(test_func.get_stats()) == 0

    def test_print_summary_method(self, capsys):
        """Test print_summary() produces output."""

        @gatto(silent=True)
        def test_func():
            return 1

        test_func()
        test_func()

        test_func.print_summary()
        captured = capsys.readouterr()
        assert len(captured.out) > 0
        assert "test_func" in captured.out

    def test_get_summary_method(self):
        """Test get_summary() returns string."""

        @gatto(silent=True)
        def test_func():
            return 1

        test_func()

        summary = test_func.get_summary()
        assert isinstance(summary, str)
        assert len(summary) > 0
        assert "test_func" in summary


class TestGattoExport:
    """Test export_stats() functionality."""

    def test_export_json(self, tmp_path):
        """Test export to JSON format."""

        @gatto(silent=True)
        def test_func(x):
            return x * 2

        test_func(5)
        test_func(10)

        output_file = tmp_path / "results.json"
        test_func.export_stats(str(output_file))

        assert output_file.exists()

        with open(output_file, "r") as f:
            data = json.load(f)

        assert data["function"] == "test_func"
        assert data["total_calls"] == 2
        assert len(data["calls"]) == 2

    def test_export_csv(self, tmp_path):
        """Test export to CSV format."""

        @gatto(silent=True)
        def test_func(n):
            return sum(range(n))

        test_func(100)
        test_func(200)
        test_func(300)

        output_file = tmp_path / "results.csv"
        test_func.export_stats(str(output_file))

        assert output_file.exists()

        with open(output_file, "r") as f:
            reader = csv.DictReader(f)
            rows = list(reader)

        assert len(rows) == 3
        assert "function_name" in rows[0]
        assert "execution_time" in rows[0]

    def test_export_markdown(self, tmp_path):
        """Test export to Markdown format."""

        @gatto(silent=True)
        def test_func():
            return [i for i in range(1000)]

        test_func()
        test_func()

        output_file = tmp_path / "results.md"
        test_func.export_stats(str(output_file))

        assert output_file.exists()

        with open(output_file, "r") as f:
            content = f.read()

        assert len(content) > 0
        assert "test_func" in content

    def test_export_auto_detect_format(self, tmp_path):
        """Test automatic format detection from extension."""

        @gatto(silent=True)
        def test_func():
            return 1

        test_func()

        # Test JSON auto-detection
        json_file = tmp_path / "auto.json"
        test_func.export_stats(str(json_file))
        assert json_file.exists()

        # Test CSV auto-detection
        csv_file = tmp_path / "auto.csv"
        test_func.export_stats(str(csv_file))
        assert csv_file.exists()

        # Test MD auto-detection
        md_file = tmp_path / "auto.md"
        test_func.export_stats(str(md_file))
        assert md_file.exists()

    def test_export_creates_directories(self, tmp_path):
        """Test that export creates parent directories."""

        @gatto(silent=True)
        def test_func():
            return 1

        test_func()

        nested_path = tmp_path / "reports" / "2024" / "results.json"
        test_func.export_stats(str(nested_path))

        assert nested_path.exists()


class TestGattoMemoryTracking:
    """Test memory tracking functionality."""

    def test_memory_tracked_by_default(self):
        """Test that memory tracking is enabled by default."""

        @gatto(silent=True)
        def allocate():
            return [i for i in range(100000)]

        allocate()
        stats = allocate.last_stats

        assert "memory_peak_mb" in stats
        assert stats["memory_peak_mb"] is not None
        assert stats["memory_peak_mb"] > 0

    def test_memory_disabled_with_show_memory_false(self):
        """Test disabling memory tracking."""

        @gatto(silent=True, show_memory=False)
        def no_memory():
            return [i for i in range(100000)]

        no_memory()
        stats = no_memory.last_stats

        assert "memory_peak_mb" not in stats

    def test_memory_values_reasonable(self):
        """Test that memory values are in reasonable ranges."""

        @gatto(silent=True)
        def small_allocation():
            return [1, 2, 3]

        small_allocation()
        stats = small_allocation.last_stats

        # Should be small but measurable
        assert stats["memory_peak_mb"] >= 0
        assert stats["memory_peak_mb"] < 100  # Shouldn't be huge for small allocation


class TestGattoArgumentCapture:
    """Test argument capture and display."""

    def test_args_captured_by_default(self):
        """Test that arguments are captured by default."""

        @gatto(silent=True)
        def with_args(a, b, c=10):
            return a + b + c

        with_args(5, 15)
        stats = with_args.last_stats

        assert "arguments" in stats
        assert stats["arguments"]["a"] == "5"
        assert stats["arguments"]["b"] == "15"
        assert stats["arguments"]["c"] == "10"

    def test_args_disabled_with_show_args_false(self):
        """Test disabling argument capture."""

        @gatto(silent=True, show_args=False)
        def no_args(x, y):
            return x * y

        no_args(3, 4)
        stats = no_args.last_stats

        assert "arguments" not in stats

    def test_args_with_complex_types(self):
        """Test argument capture with complex types."""

        @gatto(silent=True)
        def complex_args(data, config=None):
            return len(data)

        complex_args([1, 2, 3], {"key": "value"})
        stats = complex_args.last_stats

        assert "arguments" in stats
        assert "data" in stats["arguments"]
        assert "config" in stats["arguments"]

    def test_args_truncation_for_long_values(self):
        """Test that very long argument values are truncated."""

        @gatto(silent=True)
        def long_arg(data):
            return len(data)

        long_list = list(range(10000))
        long_arg(long_list)
        stats = long_arg.last_stats

        # Should be truncated with "..."
        assert len(stats["arguments"]["data"]) <= 103  # 100 chars + "..."


class TestGattoPrecision:
    """Test precision parameter."""

    def test_precision_default(self):
        """Test default precision is 6."""

        @gatto(silent=True)
        def test_func():
            time.sleep(0.001)
            return 1

        test_func()
        stats = test_func.last_stats

        # Execution time should be rounded to 6 decimal places
        assert isinstance(stats["execution_time"], float)

    def test_precision_custom(self):
        """Test custom precision setting."""

        @gatto(silent=True, precision=2)
        def test_func():
            time.sleep(0.001)
            return 1

        test_func()
        stats = test_func.last_stats

        # Should have at most 2 decimal places
        exec_time_str = str(stats["execution_time"])
        if "." in exec_time_str:
            decimal_places = len(exec_time_str.split(".")[1])
            assert decimal_places <= 2


class TestGattoEdgeCases:
    """Test edge cases and error conditions."""

    def test_empty_function(self):
        """Test decorating function that does nothing."""

        @gatto(silent=True)
        def empty():
            pass

        result = empty()
        assert result is None
        assert empty.call_count() == 1

    def test_generator_function(self):
        """Test decorating generator function."""

        @gatto(silent=True)
        def gen():
            yield 1
            yield 2
            yield 3

        result = gen()
        values = list(result)
        assert values == [1, 2, 3]

    def test_exception_in_function(self):
        """Test that exceptions are properly propagated."""

        @gatto(silent=True)
        def failing():
            raise ValueError("Test error")

        with pytest.raises(ValueError, match="Test error"):
            failing()

        # Stats should still be captured before exception
        assert failing.call_count() == 0  # Exception prevents accumulation

    def test_recursive_function(self):
        """Test decorating recursive function."""

        @gatto(silent=True)
        def factorial(n):
            if n <= 1:
                return 1
            return n * factorial(n - 1)

        result = factorial(5)
        assert result == 120
        # Each recursive call is tracked
        assert factorial.call_count() == 5

    def test_very_fast_function(self):
        """Test function that executes very quickly."""

        @gatto(silent=True)
        def instant():
            return 1

        instant()
        stats = instant.last_stats

        assert stats["execution_time"] >= 0
        assert stats["execution_time_formatted"] is not None

    def test_very_slow_function(self):
        """Test function with noticeable delay."""

        @gatto(silent=True)
        def slow():
            time.sleep(0.1)
            return "done"

        slow()
        stats = slow.last_stats

        assert stats["execution_time"] >= 0.1

    def test_multiple_returns(self):
        """Test function with multiple return points."""

        @gatto(silent=True)
        def multi_return(x):
            if x > 10:
                return "big"
            elif x > 5:
                return "medium"
            else:
                return "small"

        assert multi_return(15) == "big"
        assert multi_return(7) == "medium"
        assert multi_return(2) == "small"
        assert multi_return.call_count() == 3


class TestGattoIntegration:
    """Integration tests combining multiple features."""

    def test_full_workflow(self, tmp_path):
        """Test complete workflow: decorate, call, analyze, export."""

        @gatto(silent=True)
        def process_batch(batch_size):
            return sum(range(batch_size))

        # Execute multiple times
        for size in [100, 500, 1000, 5000]:
            process_batch(size)

        # Verify accumulation
        assert process_batch.call_count() == 4

        # Get stats
        stats = process_batch.get_stats()
        assert len(stats) == 4

        # Export
        output = tmp_path / "batch_results.json"
        process_batch.export_stats(str(output))
        assert output.exists()

        # Verify export content
        with open(output, "r") as f:
            data = json.load(f)
        assert data["total_calls"] == 4

    def test_silent_vs_verbose_mode(self, capsys):
        """Test switching between silent and verbose modes."""

        @gatto(silent=False)
        def verbose_func():
            return 1

        @gatto(silent=True)
        def silent_func():
            return 1

        verbose_func()
        verbose_output = capsys.readouterr()
        assert len(verbose_output.out) > 0

        silent_func()
        silent_output = capsys.readouterr()
        assert silent_output.out == ""

    def test_accumulation_with_clear(self):
        """Test accumulation with periodic clearing."""

        @gatto(silent=True)
        def counter():
            return 1

        # First batch
        for _ in range(5):
            counter()
        assert counter.call_count() == 5

        # Clear
        counter.clear_stats()
        assert counter.call_count() == 0

        # Second batch
        for _ in range(3):
            counter()
        assert counter.call_count() == 3

    def test_comparison_scenario(self):
        """Test comparing two different implementations."""

        @gatto(silent=True)
        def list_comp(n):
            return [x**2 for x in range(n)]

        @gatto(silent=True)
        def loop_version(n):
            result = []
            for x in range(n):
                result.append(x**2)
            return result

        # Run both multiple times
        for size in [100, 500, 1000]:
            list_comp(size)
            loop_version(size)

        # Both should have same call count
        assert list_comp.call_count() == 3
        assert loop_version.call_count() == 3

        # Can analyze separately
        lc_stats = list_comp.get_stats()
        loop_stats = loop_version.get_stats()

        assert len(lc_stats) == 3
        assert len(loop_stats) == 3


class TestGattoLastStatsAttribute:
    """Test last_stats and last_profile attributes."""

    def test_last_stats_updated(self):
        """Test that last_stats is updated after each call."""

        @gatto(silent=True)
        def test_func(x):
            return x

        test_func(1)
        first_stats = test_func.last_stats

        test_func(2)
        second_stats = test_func.last_stats

        assert first_stats != second_stats
        assert first_stats["arguments"]["x"] == "1"
        assert second_stats["arguments"]["x"] == "2"

    def test_last_profile_updated(self):
        """Test that last_profile is updated after each call."""

        @gatto(silent=True)
        def test_func():
            return 1

        test_func()
        first_profile = test_func.last_profile

        time.sleep(0.001)

        test_func()
        second_profile = test_func.last_profile

        assert first_profile != second_profile
        assert first_profile.execution_time != second_profile.execution_time
