"""
Concurrent operations integration tests.

Tests cover:
- Concurrent research requests
- Thread safety of shared resources
- Database connection pooling
- Cache concurrency
- Queue processing concurrency
- Lock management
- Resource contention handling
"""

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


class TestConcurrentResearchRequests:
    """Tests for concurrent research request handling."""

    def test_multiple_simultaneous_research_requests(self):
        """Multiple research requests should be handled simultaneously."""
        results = {}
        lock = threading.Lock()

        def run_research(research_id, query):
            time.sleep(0.01)  # Simulate work
            with lock:
                results[research_id] = {"query": query, "status": "completed"}
            return research_id

        threads = []
        for i in range(10):
            t = threading.Thread(
                target=run_research, args=(f"research_{i}", f"query_{i}")
            )
            threads.append(t)

        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(results) == 10
        for i in range(10):
            assert f"research_{i}" in results

    def test_research_requests_isolated(self):
        """Concurrent requests should be isolated from each other."""
        research_states = {}
        lock = threading.Lock()
        errors = []

        def run_research(research_id, user_id):
            try:
                with lock:
                    research_states[research_id] = {
                        "user_id": user_id,
                        "status": "started",
                    }

                time.sleep(0.01)

                with lock:
                    # Verify state wasn't modified by other threads
                    if research_states[research_id]["user_id"] != user_id:
                        errors.append(f"State corruption in {research_id}")
                    research_states[research_id]["status"] = "completed"
            except Exception as e:
                errors.append(str(e))

        threads = []
        for i in range(20):
            t = threading.Thread(
                target=run_research, args=(f"research_{i}", f"user_{i % 5}")
            )
            threads.append(t)

        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0

    def test_max_concurrent_limit_enforced(self):
        """Maximum concurrent research limit should be enforced."""
        max_concurrent = 5
        active_count = {"current": 0, "max_reached": 0}
        lock = threading.Lock()
        semaphore = threading.Semaphore(max_concurrent)

        def run_research(research_id):
            with semaphore:
                with lock:
                    active_count["current"] += 1
                    active_count["max_reached"] = max(
                        active_count["max_reached"], active_count["current"]
                    )

                time.sleep(0.05)  # Simulate work

                with lock:
                    active_count["current"] -= 1

        threads = []
        for i in range(20):
            t = threading.Thread(target=run_research, args=(f"research_{i}",))
            threads.append(t)

        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert active_count["max_reached"] <= max_concurrent


class TestDatabaseConnectionPooling:
    """Tests for database connection pooling."""

    def test_connection_pool_reuse(self):
        """Connections should be reused from pool."""
        pool = {"connections": [], "max_size": 5}
        lock = threading.Lock()
        connection_uses = []

        def get_connection():
            with lock:
                if pool["connections"]:
                    conn = pool["connections"].pop()
                    conn["reused"] = True
                    return conn
                return {"id": len(connection_uses), "reused": False}

        def release_connection(conn):
            with lock:
                if len(pool["connections"]) < pool["max_size"]:
                    pool["connections"].append(conn)

        def use_connection():
            conn = get_connection()
            with lock:
                connection_uses.append(conn)
            time.sleep(
                0.02
            )  # Increased delay to ensure some connections are released
            release_connection(conn)

        # Run in two batches to ensure reuse - first batch releases before second starts
        first_batch = [
            threading.Thread(target=use_connection) for _ in range(5)
        ]
        for t in first_batch:
            t.start()
        for t in first_batch:
            t.join()

        # Second batch should reuse connections from first batch
        second_batch = [
            threading.Thread(target=use_connection) for _ in range(5)
        ]
        for t in second_batch:
            t.start()
        for t in second_batch:
            t.join()

        # Second batch should have reused connections
        reused = sum(1 for c in connection_uses if c.get("reused"))
        assert reused > 0

    def test_connection_pool_exhaustion(self):
        """Should handle connection pool exhaustion."""
        pool_size = 3
        semaphore = threading.Semaphore(pool_size)
        waiting_threads = {"count": 0}
        lock = threading.Lock()

        def use_connection():
            with lock:
                waiting_threads["count"] += 1

            acquired = semaphore.acquire(timeout=0.1)

            with lock:
                waiting_threads["count"] -= 1

            if acquired:
                time.sleep(0.1)  # Hold connection
                semaphore.release()
                return True
            return False

        results = []
        threads = []

        for _ in range(10):
            t = threading.Thread(
                target=lambda: results.append(use_connection())
            )
            threads.append(t)

        for t in threads:
            t.start()
        for t in threads:
            t.join()

        # Some should have timed out waiting
        failed = sum(1 for r in results if not r)
        assert failed > 0


class TestCacheThreadSafety:
    """Tests for cache thread safety."""

    def test_concurrent_cache_reads(self):
        """Concurrent cache reads should be safe."""
        cache = {"key1": "value1", "key2": "value2"}
        lock = threading.RLock()
        read_results = []

        def read_cache(key):
            with lock:
                value = cache.get(key)
            read_results.append(value)

        threads = []
        for _ in range(100):
            for key in ["key1", "key2"]:
                t = threading.Thread(target=read_cache, args=(key,))
                threads.append(t)

        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(read_results) == 200

    def test_concurrent_cache_writes(self):
        """Concurrent cache writes should be safe."""
        cache = {}
        lock = threading.Lock()
        errors = []

        def write_cache(key, value):
            try:
                with lock:
                    cache[key] = value
            except Exception as e:
                errors.append(str(e))

        threads = []
        for i in range(100):
            t = threading.Thread(
                target=write_cache, args=(f"key_{i}", f"value_{i}")
            )
            threads.append(t)

        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0
        assert len(cache) == 100

    def test_cache_stampede_prevention(self):
        """Cache stampede should be prevented."""
        cache = {}
        lock = threading.Lock()
        fetch_counts = {"count": 0}
        fetch_events = {}

        def get_or_fetch(key, fetch_func):
            with lock:
                if key in cache:
                    return cache[key]

                # Check if someone else is fetching
                if key in fetch_events:
                    event = fetch_events[key]
                else:
                    event = threading.Event()
                    fetch_events[key] = event

            # If we set up the event, we do the fetch
            if not event.is_set():
                with lock:
                    if key not in cache:  # Double-check
                        value = fetch_func(key)
                        cache[key] = value
                        fetch_counts["count"] += 1
                event.set()
            else:
                event.wait()

            return cache.get(key)

        def slow_fetch(key):
            time.sleep(0.05)
            return f"value_for_{key}"

        results = []

        def fetch_key():
            result = get_or_fetch("shared_key", slow_fetch)
            results.append(result)

        threads = [threading.Thread(target=fetch_key) for _ in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        # Should only fetch once despite multiple concurrent requests
        assert fetch_counts["count"] == 1
        assert len(results) == 10


class TestQueueProcessingConcurrency:
    """Tests for queue processing concurrency."""

    def test_queue_processes_concurrently(self):
        """Queue should process items concurrently."""
        work_queue = queue.Queue()
        results = []
        lock = threading.Lock()

        def worker():
            while True:
                try:
                    item = work_queue.get(timeout=0.1)
                    time.sleep(0.01)  # Simulate work
                    with lock:
                        results.append(item)
                    work_queue.task_done()
                except queue.Empty:
                    break

        # Add work items
        for i in range(20):
            work_queue.put(f"item_{i}")

        # Start workers
        threads = [threading.Thread(target=worker) for _ in range(4)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(results) == 20

    def test_queue_order_preserved_per_user(self):
        """Queue order should be preserved per user."""
        user_queues = {}
        lock = threading.Lock()

        def add_to_queue(user_id, item):
            with lock:
                if user_id not in user_queues:
                    user_queues[user_id] = []
                user_queues[user_id].append(item)

        def process_next(user_id):
            with lock:
                if user_id in user_queues and user_queues[user_id]:
                    return user_queues[user_id].pop(0)
            return None

        # Add items
        for i in range(10):
            add_to_queue("user1", f"item_{i}")

        # Process in order
        processed = []
        while True:
            item = process_next("user1")
            if item is None:
                break
            processed.append(item)

        assert processed == [f"item_{i}" for i in range(10)]


class TestLockManagement:
    """Tests for lock management."""

    def test_lock_prevents_race_condition(self):
        """Lock should prevent race conditions."""
        counter = {"value": 0}
        lock = threading.Lock()

        def increment():
            for _ in range(1000):
                with lock:
                    counter["value"] += 1

        threads = [threading.Thread(target=increment) for _ in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert counter["value"] == 10000

    def test_deadlock_prevention_with_timeout(self):
        """Deadlocks should be prevented with lock timeout."""
        lock1 = threading.Lock()
        lock2 = threading.Lock()
        deadlocks = []

        def operation1():
            if lock1.acquire(timeout=0.1):
                time.sleep(0.05)
                if not lock2.acquire(timeout=0.1):
                    deadlocks.append("op1_lock2_timeout")
                else:
                    lock2.release()
                lock1.release()
            else:
                deadlocks.append("op1_lock1_timeout")

        def operation2():
            if lock2.acquire(timeout=0.1):
                time.sleep(0.05)
                if not lock1.acquire(timeout=0.1):
                    deadlocks.append("op2_lock1_timeout")
                else:
                    lock1.release()
                lock2.release()
            else:
                deadlocks.append("op2_lock2_timeout")

        t1 = threading.Thread(target=operation1)
        t2 = threading.Thread(target=operation2)

        t1.start()
        t2.start()
        t1.join(timeout=1)
        t2.join(timeout=1)

        # With timeouts, threads should complete (with possible timeout warnings)
        assert not t1.is_alive()
        assert not t2.is_alive()

    def test_reentrant_lock(self):
        """Reentrant lock should allow same thread to acquire multiple times."""
        lock = threading.RLock()
        acquisitions = []

        def nested_acquire():
            with lock:
                acquisitions.append(1)
                with lock:  # Same thread reacquiring
                    acquisitions.append(2)
                    with lock:  # Again
                        acquisitions.append(3)

        nested_acquire()

        assert acquisitions == [1, 2, 3]


class TestResourceContention:
    """Tests for resource contention handling."""

    def test_high_contention_handled(self):
        """High contention should be handled gracefully."""
        resource = {"value": 0}
        lock = threading.Lock()
        contention_waits = {"count": 0}

        def access_resource():
            start = time.time()
            with lock:
                elapsed = time.time() - start
                if elapsed > 0.001:  # Waited for lock
                    contention_waits["count"] += 1
                resource["value"] += 1
                time.sleep(0.001)  # Hold lock briefly

        threads = [threading.Thread(target=access_resource) for _ in range(50)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        # All should complete
        assert resource["value"] == 50
        # Some should have experienced contention
        assert contention_waits["count"] > 0

    def test_fair_resource_access(self):
        """Resource access should be reasonably fair."""
        access_counts = {}
        lock = threading.Lock()

        def access_resource(thread_id):
            for _ in range(10):
                with lock:
                    if thread_id not in access_counts:
                        access_counts[thread_id] = 0
                    access_counts[thread_id] += 1
                time.sleep(0.001)

        threads = [
            threading.Thread(target=access_resource, args=(f"thread_{i}",))
            for i in range(10)
        ]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        # Each thread should have similar access counts
        counts = list(access_counts.values())
        assert min(counts) == 10
        assert max(counts) == 10


class TestThreadPoolExecutor:
    """Tests for ThreadPoolExecutor usage."""

    def test_executor_handles_concurrent_tasks(self):
        """ThreadPoolExecutor should handle concurrent tasks."""
        results = []
        lock = threading.Lock()

        def task(task_id):
            time.sleep(0.01)
            with lock:
                results.append(task_id)
            return task_id

        with ThreadPoolExecutor(max_workers=4) as executor:
            futures = [executor.submit(task, i) for i in range(20)]
            completed = [f.result() for f in as_completed(futures)]

        assert len(completed) == 20
        assert len(results) == 20

    def test_executor_exception_handling(self):
        """Executor should handle task exceptions."""
        errors = []

        def failing_task(task_id):
            if task_id % 2 == 0:
                raise ValueError(f"Task {task_id} failed")
            return task_id

        with ThreadPoolExecutor(max_workers=4) as executor:
            futures = [executor.submit(failing_task, i) for i in range(10)]

            for future in as_completed(futures):
                try:
                    future.result()
                except ValueError as e:
                    errors.append(str(e))

        assert len(errors) == 5  # Even numbered tasks failed


class TestSocketConcurrency:
    """Tests for socket emission concurrency."""

    def test_concurrent_socket_emissions(self):
        """Concurrent socket emissions should be handled."""
        emissions = []
        lock = threading.Lock()

        def emit(event, data):
            with lock:
                emissions.append({"event": event, "data": data})

        threads = []
        for i in range(50):
            t = threading.Thread(
                target=emit, args=(f"event_{i % 5}", {"id": i})
            )
            threads.append(t)

        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(emissions) == 50

    def test_emission_ordering_per_research(self):
        """Emissions for same research should maintain order."""
        emissions = {}
        lock = threading.Lock()

        def emit(research_id, sequence):
            with lock:
                if research_id not in emissions:
                    emissions[research_id] = []
                emissions[research_id].append(sequence)

        threads = []
        for research_id in ["r1", "r2", "r3"]:
            for seq in range(10):
                t = threading.Thread(target=emit, args=(research_id, seq))
                threads.append(t)

        # Start in order but don't wait between
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        # All emissions should be recorded
        for research_id in ["r1", "r2", "r3"]:
            assert len(emissions[research_id]) == 10


class TestConcurrentSettingsAccess:
    """Tests for concurrent settings access."""

    def test_concurrent_settings_reads(self):
        """Concurrent settings reads should be safe."""
        settings = {"key1": "value1", "key2": "value2", "key3": "value3"}
        lock = threading.RLock()
        read_results = []

        def read_setting(key):
            with lock:
                value = settings.get(key)
            read_results.append((key, value))

        threads = []
        for _ in range(100):
            for key in settings.keys():
                t = threading.Thread(target=read_setting, args=(key,))
                threads.append(t)

        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(read_results) == 300

    def test_concurrent_settings_updates(self):
        """Concurrent settings updates should be safe."""
        settings = {}
        lock = threading.Lock()
        errors = []

        def update_setting(key, value):
            try:
                with lock:
                    settings[key] = value
            except Exception as e:
                errors.append(str(e))

        threads = []
        for i in range(100):
            t = threading.Thread(
                target=update_setting, args=(f"key_{i % 10}", f"value_{i}")
            )
            threads.append(t)

        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0
        assert len(settings) == 10  # 10 unique keys
