"""Tests for helper decorators used throughout the project."""

import logging
import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

import pytest

from lanscape.core.decorators import run_once, job_tracker, JobStats


@pytest.fixture(autouse=True)
def reset_job_stats():
    """Automatically reset JobStats singleton before each test."""
    JobStats.reset_for_testing()
    yield
    JobStats.reset_for_testing()


def test_run_once_caches_result_and_logs_once(caplog):
    """run_once should execute only one time and cache the return value."""
    caplog.set_level(logging.DEBUG)

    call_count = {"count": 0}

    @run_once
    def sample_function(value):
        call_count["count"] += 1
        return value * 2

    first = sample_function(3)
    second = sample_function(5)

    assert first == 6
    assert second == 6
    assert call_count["count"] == 1

    messages = [record.message for record in caplog.records]
    assert any("run_once executed" in record and "sample_function" in record for record in messages)
    assert sum("run_once executed" in record for record in messages) == 1


# JobStats Tests
#################

def test_job_stats_singleton_behavior():
    """JobStats should behave as a singleton."""
    stats1 = JobStats()
    stats2 = JobStats()

    assert stats1 is stats2
    assert id(stats1) == id(stats2)


def test_job_stats_thread_safe_singleton_creation():
    """Multiple threads creating JobStats should get the same instance."""
    instances = []
    barrier = threading.Barrier(5)

    def create_instance():
        barrier.wait()  # Synchronize thread start
        instance = JobStats()
        instances.append(instance)

    # Create multiple threads that create JobStats instances simultaneously
    threads = []
    for _ in range(5):
        thread = threading.Thread(target=create_instance)
        threads.append(thread)
        thread.start()

    # Wait for all threads to complete
    for thread in threads:
        thread.join()

    # All instances should be the same
    assert len(instances) == 5
    assert all(inst is instances[0] for inst in instances)


def test_job_stats_start_and_finish_job():
    """Test basic job tracking functionality."""
    stats = JobStats()

    # Start a job
    stats.start_job("test_function")
    assert stats.running["test_function"] == 1
    assert stats.finished["test_function"] == 0

    # Finish the job
    stats.finish_job("test_function", 1.5)
    assert stats.running.get("test_function", 0) == 0  # Should be cleaned up
    assert stats.finished["test_function"] == 1
    assert stats.timing["test_function"] == 1.5


def test_job_stats_multiple_jobs_same_function():
    """Test tracking multiple executions of the same function."""
    stats = JobStats()

    # Start and finish multiple jobs
    stats.start_job("test_func")
    stats.finish_job("test_func", 1.0)

    stats.start_job("test_func")
    stats.finish_job("test_func", 3.0)

    # Should have correct counts and average timing
    assert stats.finished["test_func"] == 2
    assert stats.timing["test_func"] == 2.0  # Average of 1.0 and 3.0


@pytest.mark.slow
def test_job_stats_concurrent_job_tracking():
    """Test thread-safe job tracking under concurrent load."""
    stats = JobStats()
    results = []

    def worker(job_id):
        func_name = f"test_job_{job_id % 3}"  # 3 different function names
        stats.start_job(func_name)
        time.sleep(0.01)  # Simulate work
        stats.finish_job(func_name, 0.01)
        return func_name

    # Run multiple concurrent workers
    with ThreadPoolExecutor(max_workers=10) as executor:
        futures = [executor.submit(worker, i) for i in range(50)]
        for future in as_completed(futures):
            results.append(future.result())

    # Verify no jobs are left running
    assert len(stats.running) == 0

    # Verify correct totals
    total_finished = sum(stats.finished.values())
    assert total_finished == 50

    # Each function should have been called multiple times
    for i in range(3):
        func_name = f"test_job_{i}"
        assert stats.finished[func_name] > 0
        assert stats.timing[func_name] > 0


def test_job_stats_clear_stats():
    """Test clearing all statistics."""
    stats = JobStats()

    # Add some data
    stats.start_job("test1")
    stats.finish_job("test1", 1.0)
    stats.start_job("test2")

    # Verify data exists
    assert len(stats.running) > 0 or len(stats.finished) > 0

    # Clear and verify
    stats.clear_stats()
    assert len(stats.running) == 0
    assert len(stats.finished) == 0
    assert len(stats.timing) == 0


def test_job_stats_get_stats_copy():
    """Test getting a thread-safe copy of statistics."""
    stats = JobStats()

    stats.start_job("test")
    stats.finish_job("test", 2.5)

    copy = stats.get_stats_copy()

    # Should be a copy, not the same objects
    assert copy['running'] is not stats.running
    assert copy['finished'] is not stats.finished
    assert copy['timing'] is not stats.timing

    # But should have the same data
    assert copy['finished']['test'] == 1
    assert copy['timing']['test'] == 2.5


# Job Tracker Decorator Tests
#############################

def test_job_tracker_basic_function_tracking():
    """Test basic function tracking with decorator."""

    @job_tracker
    def test_function():
        time.sleep(0.01)
        return "result"

    result = test_function()

    assert result == "result"

    stats = JobStats()
    assert stats.finished["test_function"] == 1
    assert stats.timing["test_function"] > 0
    assert len(stats.running) == 0  # Should be cleaned up


def test_job_tracker_method_tracking_with_class_name():
    """Test that class methods are tracked with class.method naming."""

    class TestClass:
        """Test class for method tracking."""

        @job_tracker
        def test_method(self):
            """Test method that returns a string."""
            time.sleep(0.01)
            return "method_result"

    obj = TestClass()
    result = obj.test_method()

    assert result == "method_result"

    stats = JobStats()
    assert stats.finished["TestClass.test_method"] == 1
    assert stats.timing["TestClass.test_method"] > 0


def test_job_tracker_exception_handling():
    """Test that statistics are updated even when function raises exception."""

    @job_tracker
    def failing_function():
        time.sleep(0.01)
        raise ValueError("Test exception")

    with pytest.raises(ValueError):
        failing_function()

    stats = JobStats()
    assert stats.finished["failing_function"] == 1
    assert stats.timing["failing_function"] > 0
    assert len(stats.running) == 0  # Should be cleaned up


@pytest.mark.slow
def test_job_tracker_concurrent_decorated_functions():
    """Test decorator behavior under concurrent execution."""

    @job_tracker
    def concurrent_task(task_id):
        time.sleep(0.01)
        return task_id

    results = []
    with ThreadPoolExecutor(max_workers=5) as executor:
        futures = [executor.submit(concurrent_task, i) for i in range(20)]
        for future in as_completed(futures):
            results.append(future.result())

    assert len(results) == 20

    stats = JobStats()
    assert stats.finished["concurrent_task"] == 20
    assert stats.timing["concurrent_task"] > 0
    assert len(stats.running) == 0  # All should be cleaned up


@pytest.mark.parametrize("execution_count", [1, 2, 3])
def test_job_tracker_multiple_executions(execution_count):
    """Test tracking multiple executions of the same function."""

    @job_tracker
    def repeated_function():
        time.sleep(0.01)
        return "result"

    for _ in range(execution_count):
        repeated_function()

    stats = JobStats()
    assert stats.finished["repeated_function"] == execution_count
    assert stats.timing["repeated_function"] > 0


def test_job_tracker_multiple_different_functions():
    """Test tracking multiple different decorated functions."""

    @job_tracker
    def function_a():
        time.sleep(0.1)
        return "a"

    @job_tracker
    def function_b():
        time.sleep(0.3)
        return "b"

    function_a()
    function_b()
    function_a()

    stats = JobStats()
    assert stats.finished["function_a"] == 2
    assert stats.finished["function_b"] == 1
    assert stats.timing["function_a"] > 0
    assert stats.timing["function_b"] > stats.timing["function_a"]  # b sleeps longer


@pytest.mark.parametrize("function_name,expected_calls", [
    ("function_a", 2),
    ("function_b", 1),
])
def test_job_tracker_parametrized_function_calls(function_name, expected_calls):
    """Test parametrized validation of function call counts."""

    @job_tracker
    def function_a():
        return "a"

    @job_tracker
    def function_b():
        return "b"

    # Execute functions based on expected calls
    if function_name == "function_a":
        for _ in range(expected_calls):
            function_a()
    elif function_name == "function_b":
        for _ in range(expected_calls):
            function_b()

    stats = JobStats()
    assert stats.finished[function_name] == expected_calls
