#!/usr/bin/env python3
"""
Test Per-User Queue System

Tests:
1. Per-user lock acquisition and release
2. Lock timeout and cleanup
3. Queue controller with per-user serialization
4. Fair scheduling respects per-user locks
5. Conductor lock cleanup
"""

import sys
import json
import time
from pathlib import Path
from datetime import datetime, timedelta

# Add lib to path
lib_path = Path(__file__).parent.parent / "lib"
sys.path.insert(0, str(lib_path))

from per_user_queue_manager import PerUserQueueManager
from queue_controller_v2 import QueueControllerV2
from conductor_lock_cleanup import ConductorLockCleanup


def test_per_user_lock_basic():
    """Test basic lock acquire and release."""
    print("\n=== Test: Basic Lock Acquire/Release ===")

    manager = PerUserQueueManager()
    user = "testuser"
    task_id = "task_123"

    # Acquire lock
    acquired, lock_id = manager.acquire_lock(user, task_id)
    assert acquired, f"Failed to acquire lock for {user}"
    assert lock_id, "Lock ID should not be None"
    print(f"✓ Acquired lock: user={user}, lock_id={lock_id}")

    # Check lock is active
    assert manager.is_user_locked(user), "User should be locked"
    print(f"✓ User is locked")

    # Get lock info
    lock_info = manager.get_lock_info(user)
    assert lock_info, "Should return lock info"
    assert lock_info["user"] == user
    print(f"✓ Lock info retrieved: {lock_info['lock_id']}")

    # Release lock
    released = manager.release_lock(user, lock_id)
    assert released, "Failed to release lock"
    print(f"✓ Released lock")

    # Check lock is gone
    assert not manager.is_user_locked(user), "User should not be locked"
    print(f"✓ Lock released successfully")


def test_concurrent_lock_contention():
    """Test that only one lock per user can be held."""
    print("\n=== Test: Concurrent Lock Contention ===")

    manager = PerUserQueueManager()
    user = "contentionuser"

    # Acquire first lock
    acquired1, lock_id1 = manager.acquire_lock(user, "task_1", timeout=1)
    assert acquired1, "First lock should succeed"
    print(f"✓ First lock acquired: {lock_id1}")

    # Try to acquire second lock (should timeout)
    acquired2, lock_id2 = manager.acquire_lock(user, "task_2", timeout=1)
    assert not acquired2, "Second lock should fail due to contention"
    assert lock_id2 is None
    print(f"✓ Second lock correctly rejected (contention)")

    # Release first lock
    manager.release_lock(user, lock_id1)
    print(f"✓ First lock released")

    # Now second should succeed
    acquired3, lock_id3 = manager.acquire_lock(user, "task_2", timeout=1)
    assert acquired3, "Third lock should succeed after release"
    print(f"✓ Third lock acquired after release: {lock_id3}")

    manager.release_lock(user, lock_id3)


def test_stale_lock_cleanup():
    """Test stale lock detection and cleanup."""
    print("\n=== Test: Stale Lock Cleanup ===")

    manager = PerUserQueueManager()
    user = "staleuser"

    # Acquire lock with custom timeout
    acquired, lock_id = manager.acquire_lock(user, "task_stale")
    assert acquired
    print(f"✓ Lock acquired: {lock_id}")

    # Manually set lock as expired
    lock_meta_path = manager._get_lock_meta_path(user)
    meta = json.loads(lock_meta_path.read_text())
    meta["expires_at"] = (datetime.now() - timedelta(hours=1)).isoformat()
    lock_meta_path.write_text(json.dumps(meta))
    print(f"✓ Lock manually set as stale")

    # Should be detected as stale
    assert manager._is_lock_stale(user), "Lock should be detected as stale"
    print(f"✓ Stale lock detected")

    # Cleanup should remove it
    manager._cleanup_stale_locks(user)
    assert not manager.is_user_locked(user), "Stale lock should be cleaned up"
    print(f"✓ Stale lock cleaned up")


def test_multiple_users():
    """Test that different users have independent locks."""
    print("\n=== Test: Multiple Users Independence ===")

    manager = PerUserQueueManager()

    # Acquire locks for different users
    acquired1, lock_id1 = manager.acquire_lock("user_a", "task_a")
    acquired2, lock_id2 = manager.acquire_lock("user_b", "task_b")

    assert acquired1 and acquired2, "Both locks should succeed"
    print(f"✓ Acquired locks for user_a and user_b")

    # Both should be locked
    assert manager.is_user_locked("user_a"), "user_a should be locked"
    assert manager.is_user_locked("user_b"), "user_b should be locked"
    print(f"✓ Both users are locked")

    # Release user_a's lock
    manager.release_lock("user_a", lock_id1)
    assert not manager.is_user_locked("user_a"), "user_a should be unlocked"
    assert manager.is_user_locked("user_b"), "user_b should still be locked"
    print(f"✓ user_a released, user_b still locked")

    manager.release_lock("user_b", lock_id2)


def test_queue_controller_v2():
    """Test QueueControllerV2 with per-user serialization."""
    print("\n=== Test: QueueControllerV2 Integration ===")

    qc = QueueControllerV2()

    # Ensure per-user serialization is in config and enabled for testing
    if "per_user_serialization" not in qc.config:
        qc.config["per_user_serialization"] = {"enabled": True, "lock_timeout_seconds": 3600}
    qc.config["per_user_serialization"]["enabled"] = True

    # Enqueue tasks for different projects (users)
    task_id_1, pos_1 = qc.enqueue("project_a", "Task 1 for project A")
    task_id_2, pos_2 = qc.enqueue("project_b", "Task 1 for project B")
    task_id_3, pos_3 = qc.enqueue("project_a", "Task 2 for project A")

    print(f"✓ Enqueued 3 tasks")
    print(f"  - project_a: {task_id_1} (pos {pos_1}), {task_id_3} (pos {pos_3})")
    print(f"  - project_b: {task_id_2} (pos {pos_2})")

    # Get queue status
    status = qc.get_queue_status()
    initial_pending = status["pending"]["total"]
    assert initial_pending >= 3, f"Should have at least 3 pending tasks, have {initial_pending}"
    print(f"✓ Queue status: {initial_pending} total pending tasks (at least 3 new ones)")

    # Check that per-user locks are respected
    user_a = qc.extract_user_from_project("project_a")
    user_b = qc.extract_user_from_project("project_b")

    can_exec_a = qc.can_user_execute_task(user_a)
    can_exec_b = qc.can_user_execute_task(user_b)

    assert can_exec_a and can_exec_b, "Both users should be able to execute"
    print(f"✓ Both users can execute tasks")

    # Acquire locks
    acq_a, lock_a = qc.acquire_user_lock(user_a, task_id_1)
    assert acq_a and lock_a, "Should acquire lock for user_a"
    print(f"✓ Acquired lock for user_a: {lock_a}")

    # Now user_a cannot execute another task
    can_exec_a2 = qc.can_user_execute_task(user_a)
    assert not can_exec_a2, "user_a should not be able to execute while locked"
    print(f"✓ user_a locked, cannot execute new tasks")

    # But user_b can
    can_exec_b2 = qc.can_user_execute_task(user_b)
    assert can_exec_b2, "user_b should still be able to execute"
    print(f"✓ user_b can still execute")

    # Release user_a's lock
    qc.release_user_lock(user_a, lock_a)
    can_exec_a3 = qc.can_user_execute_task(user_a)
    assert can_exec_a3, "user_a should be able to execute again"
    print(f"✓ Released user_a lock, can execute again")


def test_fair_scheduling_with_locks():
    """Test that fair scheduling respects per-user locks."""
    print("\n=== Test: Fair Scheduling with Per-User Locks ===")

    qc = QueueControllerV2()

    # Ensure per-user serialization is in config and enabled for testing
    if "per_user_serialization" not in qc.config:
        qc.config["per_user_serialization"] = {"enabled": True, "lock_timeout_seconds": 3600}
    qc.config["per_user_serialization"]["enabled"] = True

    # Enqueue multiple tasks
    task_id_1, _ = qc.enqueue("proj_a", "Task A1", priority=5)
    task_id_2, _ = qc.enqueue("proj_b", "Task B1", priority=5)
    task_id_3, _ = qc.enqueue("proj_a", "Task A2", priority=5)

    # Get pending tasks
    capacity = qc._read_capacity()
    task = qc._select_next_task(capacity)

    assert task, "Should select a task"
    print(f"✓ Selected task: {task['id']} for {task['project']}")

    # Acquire lock for this task's user
    user = task.get("user") or qc.extract_user_from_project(task["project"])
    acq, lock_id = qc.acquire_user_lock(user, task["id"])
    assert acq, "Should acquire user lock"

    # Now selecting next task should skip tasks for this user
    # and select from another user
    task2 = qc._select_next_task(capacity)

    if task2:
        user2 = task2.get("user") or qc.extract_user_from_project(task2["project"])
        # Task should be from a different user or None
        assert user2 != user, f"Should select different user, got {user2}"
        print(f"✓ Fair scheduling respects user lock: skipped {user}, selected {user2}")
    else:
        print(f"✓ Fair scheduling: no available task (all from locked user)")

    qc.release_user_lock(user, lock_id)


def run_all_tests():
    """Run all tests."""
    print("=" * 60)
    print("Per-User Queue System Tests")
    print("=" * 60)

    tests = [
        test_per_user_lock_basic,
        test_concurrent_lock_contention,
        test_stale_lock_cleanup,
        test_multiple_users,
        test_queue_controller_v2,
        test_fair_scheduling_with_locks,
    ]

    passed = 0
    failed = 0

    for test_func in tests:
        try:
            test_func()
            passed += 1
        except AssertionError as e:
            print(f"✗ FAILED: {e}")
            failed += 1
        except Exception as e:
            print(f"✗ ERROR: {e}")
            failed += 1

    print("\n" + "=" * 60)
    print(f"Results: {passed} passed, {failed} failed")
    print("=" * 60)

    return failed == 0


if __name__ == "__main__":
    success = run_all_tests()
    sys.exit(0 if success else 1)
