using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace GeoSus.Client
{
    /// <summary>
/// Comprehensive headless simulator client for testing all game aspects.
/// Supports both autonomous simulation and step-by-step controlled testing.
/// </summary>
public class SimulatorClient : IDisposable
{
    private readonly GameClient _client;
    private readonly Random _random = new Random();
    private CancellationTokenSource? _cts;
    private Task? _simulationTask;
    private PlayerState _myState = PlayerState.Alive;
    
    #region Public Properties
    
    public string ClientUuid => _client.ClientUuid;
    public string DisplayName => _client.DisplayName;
    public bool IsConnected => _client.IsConnected;
    public LobbyState? LobbyState => _client.CurrentLobbyState;
    public string? LobbyId => _client.LobbyId;
    public string? JoinCode => _client.JoinCode;
    public PlayerRole? Role => _client.MyRole;
    public PlayerState State => _myState;
    public Position Position => _client.MyPosition;
    public bool IsAlive => _myState == PlayerState.Alive;
    public bool IsDead => _myState == PlayerState.Dead;
    public bool IsImpostor => _client.MyRole == PlayerRole.Impostor;
    public bool IsCrew => _client.MyRole == PlayerRole.Crew;
    
    // Tasks
    public List<GameTask> MyTasks => _client.MyTasks;
    public HashSet<string> CompletedTaskIds { get; } = new HashSet<string>();
    public string? CurrentTaskId { get; private set; }
    
    // Game state tracking
    public int TotalKills { get; private set; }
    public int TasksCompleted { get; private set; }
    public int MeetingsAttended { get; private set; }
    public int VotesCast { get; private set; }
    public int BodiesReported { get; private set; }
    public bool WasEjected { get; private set; }
    public bool WasKilled { get; private set; }
    public string? LastError { get; private set; }
    public string? GameResult { get; private set; }
    public string? WinningFaction { get; private set; }
    public bool GameEnded => GameResult != null;
    
    // Nearby entities
    public Dictionary<string, PlayerPositionInfo> NearbyPlayers => _client.PlayerPositions;
    public List<Body> NearbyBodies => _client.Bodies;
    
    // Meeting state
    public bool InMeeting { get; private set; }
    public bool HasVotedThisMeeting { get; private set; }
    public string? CurrentMeetingId { get; private set; }
    public Position? MeetingLocation { get; private set; }
    public DateTime? MeetingVotingEndTime { get; private set; }
    public DateTime? MeetingDiscussionEndTime { get; private set; }
    
    /// <summary>
    /// Returns true if the discussion phase has ended and voting can begin.
    /// </summary>
    public bool CanVote => InMeeting && 
        (!MeetingDiscussionEndTime.HasValue || DateTime.UtcNow >= MeetingDiscussionEndTime.Value);
    
    // Current game phase from lobby state
    public string GamePhase => _client.CurrentLobbyState?.Phase.ToString() ?? "Unknown";
    
    // Sabotage state
    public bool SabotageActive { get; private set; }
    public string? CurrentSabotageId { get; private set; }
    public SabotageType? CurrentSabotageType { get; private set; }
    public DateTime? SabotageDeadline { get; private set; }
    public List<RepairStationInfo> RepairStations { get; private set; } = new List<RepairStationInfo>();
    public int SabotagesStarted { get; private set; }
    public int SabotagesRepaired { get; private set; }
    public bool IsRepairing { get; private set; }
    public string? RepairingStationId { get; private set; }
    
    /// <summary>
    /// Returns true if comms are blocked (can't report/meeting)
    /// </summary>
    public bool IsCommsBlocked => SabotageActive && CurrentSabotageType == SabotageType.CommsBlackout;
    
    /// <summary>
    /// Returns true if there's a critical meltdown countdown
    /// </summary>
    public bool IsMeltdownActive => SabotageActive && CurrentSabotageType == SabotageType.CriticalMeltdown;
    
    #endregion
    
    #region Events
    
    public event Action<string>? OnLog;
    public event Action<string>? OnError;
    public event Action<string, object?>? OnGameEvent;
    public event Action? OnKilled;
    public event Action? OnEjected;
    public event Action<string>? OnGameEnded;
    public event Action? OnMeetingStarted;
    public event Action? OnMeetingEnded;
    public event Action<SabotageType>? OnSabotageStarted;
    public event Action<SabotageType>? OnSabotageRepaired;
    public event Action? OnMeltdown;
    
    #endregion
    
    #region Constructor
    
    public SimulatorClient(string clientUuid, string displayName)
    {
        _client = new GameClient(clientUuid, displayName);
        
        _client.OnConnected += () => Log("Připojen k serveru");
        _client.OnDisconnected += (reason) => Log($"Odpojen: {reason}");
        _client.OnError += (error) => 
        {
            LastError = error;
            Log($"Chyba: {error}");
            OnError?.Invoke(error);
        };
        
        _client.OnMessage += HandleMessage;
        _client.OnGameEvent += HandleGameEvent;
    }
    
    #endregion
    
    #region Message Handlers
    
    private void HandleMessage(Message msg)
    {
        switch (msg)
        {
            case CreateLobbyResponse r:
                if (r.Success)
                    Log($"Lobby vytvořeno: {r.JoinCode}");
                else
                    LogError($"Lobby creation failed");
                break;
                
            case JoinLobbyResponse r:
                if (r.Success)
                    Log($"Připojen do lobby: {r.LobbyId}");
                else
                    LogError("Join lobby failed");
                break;
                
            case Ack a when !a.Success:
                LastError = a.Error;
                Log($"Akce zamítnuta: {a.Error}");
                break;
        }
    }
    
    private void HandleGameEvent(GameEvent evt)
    {
        OnGameEvent?.Invoke(evt.EventType, evt.Payload);
        
        switch (evt.EventType)
        {
            case "GameStarting":
                Log("Game loading - fetching map data...");
                break;
                
            case "MapDataReady":
                var mapPayload = evt.GetPayload<MapDataReadyPayload>();
                var buildingCount = mapPayload?.MapData?.Buildings?.Count ?? 0;
                var pathwayCount = mapPayload?.MapData?.Pathways?.Count ?? 0;
                Log($"Map data received: {buildingCount} buildings, {pathwayCount} pathways - sending confirmation");
                break;
                
            case "PlayerMapDataReceived":
                var progressPayload = evt.GetPayload<PlayerMapDataReceivedPayload>();
                if (progressPayload != null)
                {
                    Log($"Player {progressPayload.DisplayName} ready ({progressPayload.PlayersReady}/{progressPayload.TotalPlayers})");
                }
                break;
                
            case "GameStarted":
                Log("Hra začala!");
                break;
                
            case "RoleAssigned":
                var rolePayload = evt.GetPayload<RoleAssignedPayload>();
                if (rolePayload?.ClientUuid == ClientUuid)
                {
                    Log($"Moje role: {rolePayload.Role}");
                }
                break;
                
            case "PlayerKilled":
                var killPayload = evt.GetPayload<PlayerKilledPayload>();
                Log($"Hráč {killPayload?.VictimId} byl zabit");
                if (killPayload?.VictimId == ClientUuid)
                {
                    WasKilled = true;
                    _myState = PlayerState.Dead;
                    Log("BYL JSEM ZABIT!");
                    OnKilled?.Invoke();
                }
                break;
                
            case "MeetingStarted":
                var meetingPayload = evt.GetPayload<MeetingStartedPayload>();
                InMeeting = true;
                HasVotedThisMeeting = false;
                CurrentMeetingId = meetingPayload?.MeetingId;
                MeetingLocation = meetingPayload?.MeetingLocation;
                MeetingVotingEndTime = meetingPayload?.VotingEndTime;
                MeetingDiscussionEndTime = meetingPayload?.DiscussionEndTime;
                Log($"MEETING ZAČAL! Typ: {meetingPayload?.Type}, Lokace: {MeetingLocation?.Lat:F4},{MeetingLocation?.Lon:F4}");
                if (MeetingDiscussionEndTime.HasValue)
                {
                    var discussionMs = (MeetingDiscussionEndTime.Value - DateTime.UtcNow).TotalMilliseconds;
                    Log($"  Diskuze do: {MeetingDiscussionEndTime.Value:HH:mm:ss} ({discussionMs:F0}ms)");
                }
                OnMeetingStarted?.Invoke();
                break;
                
            case "VotingStarted":
                Log("Hlasování začalo!");
                break;
                
            case "PlayerVoted":
                var voteInfoPayload = evt.GetPayload<PlayerVotedPayload>();
                Log($"Hráč {voteInfoPayload?.VoterId} hlasoval");
                break;
                
            case "VotingClosed":
                var votePayload = evt.GetPayload<VotingClosedPayload>();
                InMeeting = false;
                CurrentMeetingId = null;
                MeetingLocation = null;
                MeetingVotingEndTime = null;
                MeetingDiscussionEndTime = null;
                if (votePayload?.EjectedPlayerId != null)
                {
                    Log($"Hráč {votePayload.EjectedPlayerId} byl VYHOZEN! (remíza: {votePayload.WasTie})");
                    if (votePayload.EjectedPlayerId == ClientUuid)
                    {
                        WasEjected = true;
                        _myState = PlayerState.Dead;
                        Log("BYL JSEM VYHOZEN!");
                        OnEjected?.Invoke();
                    }
                }
                else
                {
                    Log("Nikdo nebyl vyhozen (skip nebo remíza)");
                }
                OnMeetingEnded?.Invoke();
                break;
                
            case "TaskCompleted":
                var taskPayload = evt.GetPayload<TaskCompletedPayload>();
                if (taskPayload?.ClientUuid == ClientUuid)
                {
                    TasksCompleted++;
                    CompletedTaskIds.Add(taskPayload.TaskId);
                    CurrentTaskId = null;
                    Log($"TASK DOKONČEN: {taskPayload.TaskId} (celkem: {TasksCompleted})");
                }
                break;
                
            case "GameEnded":
                var endPayload = evt.GetPayload<GameEndedPayload>();
                GameResult = endPayload?.Reason;
                WinningFaction = endPayload?.WinningFaction;
                Log($"=== HRA SKONČILA! Vítěz: {endPayload?.WinningFaction} - {endPayload?.Reason} ===");
                OnGameEnded?.Invoke(endPayload?.WinningFaction ?? "Unknown");
                break;
                
            // Sabotage events
            case "SabotageStarted":
                var sabStartPayload = evt.GetPayload<SabotageStartedPayload>();
                if (sabStartPayload != null)
                {
                    SabotageActive = true;
                    CurrentSabotageId = sabStartPayload.SabotageId;
                    CurrentSabotageType = sabStartPayload.Type;
                    SabotageDeadline = sabStartPayload.Deadline;
                    RepairStations = sabStartPayload.RepairStations;
                    
                    Log($"⚠ SABOTÁŽ SPUŠTĚNA: {sabStartPayload.Type}!");
                    if (sabStartPayload.Deadline.HasValue)
                    {
                        var remaining = (sabStartPayload.Deadline.Value - DateTime.UtcNow).TotalSeconds;
                        Log($"  ⏱ DEADLINE: {remaining:F0}s - musíte opravit nebo prohrajete!");
                    }
                    foreach (var station in sabStartPayload.RepairStations)
                    {
                        Log($"  📍 Stanice {station.Name}: {station.Location.Lat:F4},{station.Location.Lon:F4}");
                    }
                    OnSabotageStarted?.Invoke(sabStartPayload.Type);
                }
                break;
                
            case "RepairStarted":
                var repStartPayload = evt.GetPayload<RepairStartedPayload>();
                if (repStartPayload?.PlayerId == ClientUuid)
                {
                    IsRepairing = true;
                    RepairingStationId = repStartPayload.StationId;
                    Log($"🔧 Zahájil jsem opravu stanice {repStartPayload.StationId}");
                }
                else
                {
                    Log($"🔧 Hráč {repStartPayload?.PlayerId} opravuje {repStartPayload?.StationId}");
                }
                break;
                
            case "RepairStopped":
                var repStopPayload = evt.GetPayload<RepairStoppedPayload>();
                if (repStopPayload?.PlayerId == ClientUuid)
                {
                    IsRepairing = false;
                    RepairingStationId = null;
                    Log($"❌ Oprava přerušena: {repStopPayload.StationId}");
                }
                break;
                
            case "SabotageRepaired":
            case "SabotageExpired":
                var sabRepPayload = evt.GetPayload<SabotageRepairedPayload>();
                if (sabRepPayload != null)
                {
                    SabotageActive = false;
                    CurrentSabotageId = null;
                    CurrentSabotageType = null;
                    SabotageDeadline = null;
                    RepairStations.Clear();
                    IsRepairing = false;
                    RepairingStationId = null;
                    SabotagesRepaired++;
                    
                    var repairers = sabRepPayload.RepairerIds.Count > 0 
                        ? string.Join(", ", sabRepPayload.RepairerIds)
                        : "auto-expire";
                    Log($"✅ SABOTÁŽ OPRAVENA: {sabRepPayload.Type} (opravili: {repairers})");
                    OnSabotageRepaired?.Invoke(sabRepPayload.Type);
                }
                break;
                
            case "SabotageMeltdown":
                var meltdownPayload = evt.GetPayload<SabotageMeltdownPayload>();
                Log($"💥 MELTDOWN! Sabotáž nebyla opravena včas - Impostoři vyhráli!");
                OnMeltdown?.Invoke();
                break;
        }
    }
    
    #endregion
    
    #region Connection & Lobby
    
    public async Task<bool> ConnectAsync(string host, int port)
    {
        return await _client.ConnectAsync(host, port);
    }
    
    public void Disconnect()
    {
        StopSimulation();
        _client.Disconnect();
    }
    
    public void Update()
    {
        _client.Update();
    }
    
    public void CreateLobby(Position center, int impostorCount = 1, int taskCount = 5, string? password = null)
    {
        _client.CreateLobby(center, impostorCount, taskCount, password);
    }
    
    /// <summary>
    /// Async wrapper for CreateLobby - waits for lobby to be created
    /// </summary>
    public async Task<bool> CreateLobbyAsync(string? password, Position center, double radius = 500, int impostorCount = 1, int taskCount = 5)
    {
        _client.CreateLobby(center, impostorCount, taskCount, password, radius);
        
        // Wait for lobby creation response
        for (int i = 0; i < 50; i++) // 5 seconds timeout
        {
            Update();
            if (!string.IsNullOrEmpty(JoinCode))
                return true;
            if (LastError != null && LastError.Contains("lobby"))
                return false;
            await Task.Delay(100);
        }
        return false;
    }
    
    public void JoinLobby(string joinCode, string? password = null)
    {
        _client.JoinLobby(joinCode, password);
    }
    
    /// <summary>
    /// Async wrapper for JoinLobby - waits for join confirmation
    /// </summary>
    public async Task<bool> JoinLobbyAsync(string joinCode, string? password = null)
    {
        _client.JoinLobby(joinCode, password);
        
        // Wait for join response
        for (int i = 0; i < 50; i++)
        {
            Update();
            if (!string.IsNullOrEmpty(LobbyId))
                return true;
            if (LastError != null && LastError.Contains("join"))
                return false;
            await Task.Delay(100);
        }
        return false;
    }
    
    public void LeaveLobby()
    {
        _client.LeaveLobby();
    }
    
    public void StartGame()
    {
        _client.StartGame();
    }
    
    /// <summary>
    /// Async wrapper for StartGame - waits for game to start
    /// </summary>
    public async Task<bool> StartGameAsync()
    {
        _client.StartGame();
        
        // Wait for game start
        for (int i = 0; i < 50; i++)
        {
            Update();
            if (Role.HasValue)
                return true;
            await Task.Delay(100);
        }
        return false;
    }
    
    #endregion
    
    #region Movement
    
    public void MoveTo(Position position)
    {
        _client.UpdatePosition(position);
    }
    
    public void MoveTowards(Position target, double maxDistanceMeters)
    {
        var current = Position;
        var distance = current.DistanceTo(target);
        
        if (distance <= maxDistanceMeters)
        {
            MoveTo(target);
        }
        else
        {
            var ratio = maxDistanceMeters / distance;
            var newPos = new Position(
                current.Lat + (target.Lat - current.Lat) * ratio,
                current.Lon + (target.Lon - current.Lon) * ratio
            );
            MoveTo(newPos);
        }
    }
    
    public Position GetRandomPositionNear(Position center, double radiusMeters)
    {
        var angle = _random.NextDouble() * 2 * Math.PI;
        var distance = _random.NextDouble() * radiusMeters;
        var lat = center.Lat + (distance / 111000) * Math.Cos(angle);
        var lon = center.Lon + (distance / (111000 * Math.Cos(center.Lat * Math.PI / 180))) * Math.Sin(angle);
        return new Position(lat, lon);
    }
    
    #endregion
    
    #region Kill Actions (Impostor)
    
    public bool TryKill(string targetUuid)
    {
        if (!IsImpostor || !IsAlive)
        {
            Log("Nemohu zabíjet - nejsem živý impostor");
            return false;
        }
        
        Log($">>> POKUS O ZABITÍ: {targetUuid}");
        _client.Kill(targetUuid);
        TotalKills++;
        return true;
    }
    
    public string? FindKillTarget(double maxDistance = 5.0)
    {
        return _client.FindNearbyPlayer(maxDistance, aliveOnly: true);
    }
    
    public bool TryKillNearby(double maxDistance = 5.0)
    {
        var target = FindKillTarget(maxDistance);
        if (target != null)
        {
            return TryKill(target);
        }
        return false;
    }
    
    #endregion
    
    #region Report & Meeting Actions
    
    public bool TryReportBody(string bodyId)
    {
        if (!IsAlive)
        {
            Log("Nemohu reportovat - jsem mrtvý");
            return false;
        }
        
        Log($">>> REPORTUJI TĚLO: {bodyId}");
        _client.ReportBody(bodyId);
        BodiesReported++;
        return true;
    }
    
    public Body? FindNearbyBody(double maxDistance = 5.0)
    {
        return _client.FindNearbyBody(maxDistance);
    }
    
    public bool TryReportNearbyBody(double maxDistance = 5.0)
    {
        var body = FindNearbyBody(maxDistance);
        if (body != null)
        {
            return TryReportBody(body.BodyId);
        }
        return false;
    }
    
    public bool TryCallEmergencyMeeting()
    {
        if (!IsAlive)
        {
            Log("Nemohu svolat meeting - jsem mrtvý");
            return false;
        }
        
        Log(">>> SVOLÁVÁM EMERGENCY MEETING!");
        _client.CallEmergencyMeeting();
        return true;
    }
    
    #endregion
    
    #region Voting Actions
    
    public bool TryVote(string? targetUuid)
    {
        if (!IsAlive)
        {
            Log("Nemohu hlasovat - jsem mrtvý");
            return false;
        }
        
        if (!InMeeting)
        {
            Log("Nemohu hlasovat - není meeting");
            return false;
        }
        
        var voteTarget = targetUuid ?? "SKIP";
        Log($">>> HLASUJI PRO: {voteTarget}");
        _client.Vote(targetUuid);
        VotesCast++;
        HasVotedThisMeeting = true;
        MeetingsAttended++;
        return true;
    }
    
    public bool TryVoteSkip()
    {
        return TryVote(null);
    }
    
    public bool TryVoteRandom()
    {
        if (!InMeeting || !IsAlive) return false;
        
        // Pick a random alive player (or skip)
        var alivePlayers = NearbyPlayers.Values
            .Where(p => p.State == PlayerState.Alive && p.ClientUuid != ClientUuid)
            .ToList();
            
        if (alivePlayers.Count == 0 || _random.NextDouble() < 0.3)
        {
            return TryVoteSkip();
        }
        
        var target = alivePlayers[_random.Next(alivePlayers.Count)];
        return TryVote(target.ClientUuid);
    }
    
    /// <summary>
    /// Vote for the player with the most suspicion (for crew) or a random crew (for impostor)
    /// </summary>
    public bool TryVoteSmart()
    {
        if (!InMeeting || !IsAlive) return false;
        
        var alivePlayers = NearbyPlayers.Values
            .Where(p => p.State == PlayerState.Alive && p.ClientUuid != ClientUuid)
            .ToList();
        
        if (alivePlayers.Count == 0)
        {
            return TryVoteSkip();
        }
        
        // Impostors vote randomly among non-impostors or skip
        if (IsImpostor)
        {
            if (_random.NextDouble() < 0.5)
            {
                return TryVoteSkip();
            }
            var target = alivePlayers[_random.Next(alivePlayers.Count)];
            return TryVote(target.ClientUuid);
        }
        
        // Crew votes randomly for now (could be smarter with suspicion tracking)
        if (_random.NextDouble() < 0.2)
        {
            return TryVoteSkip();
        }
        var crewTarget = alivePlayers[_random.Next(alivePlayers.Count)];
        return TryVote(crewTarget.ClientUuid);
    }
    
    #endregion
    
    #region Task Actions
    
    public bool TryCompleteTask(string taskId)
    {
        if (IsImpostor)
        {
            Log($"Nemohu dělat tasky - jsem impostor");
            return false;
        }
        
        if (CompletedTaskIds.Contains(taskId))
        {
            Log($"Task {taskId} již dokončen");
            return false;
        }
        
        Log($">>> DOKONČUJI TASK: {taskId}");
        _client.CompleteTask(taskId);
        return true;
    }
    
    public GameTask? FindNearbyTask(double maxDistance = 5.0)
    {
        foreach (var task in MyTasks)
        {
            if (CompletedTaskIds.Contains(task.TaskId)) continue;
            
            if (Position.DistanceTo(task.Location) <= maxDistance)
            {
                return task;
            }
        }
        return null;
    }
    
    public GameTask? GetNextIncompleteTask()
    {
        return MyTasks.FirstOrDefault(t => !CompletedTaskIds.Contains(t.TaskId));
    }
    
    public int GetRemainingTaskCount()
    {
        return MyTasks.Count - CompletedTaskIds.Count;
    }
    
    #endregion
    
    #region Sabotage Actions (Impostor)
    
    /// <summary>
    /// Start a sabotage (impostor only)
    /// </summary>
    public bool TrySabotage(SabotageType sabotageType)
    {
        if (!IsImpostor)
        {
            Log("Nemohu sabotovat - nejsem impostor");
            return false;
        }
        
        if (!IsAlive)
        {
            Log("Nemohu sabotovat - jsem mrtvý");
            return false;
        }
        
        if (SabotageActive)
        {
            Log($"Nemohu sabotovat - již probíhá sabotáž: {CurrentSabotageType}");
            return false;
        }
        
        if (InMeeting)
        {
            Log("Nemohu sabotovat - probíhá meeting");
            return false;
        }
        
        Log($">>> SPOUŠTÍM SABOTÁŽ: {sabotageType}");
        _client.Send(new StartSabotage { SabotageType = sabotageType });
        SabotagesStarted++;
        return true;
    }
    
    /// <summary>
    /// Start repairing at a repair station (crew or impostor can repair)
    /// </summary>
    public bool TryStartRepair(string stationId)
    {
        if (!IsAlive)
        {
            Log("Nemohu opravovat - jsem mrtvý");
            return false;
        }
        
        if (!SabotageActive)
        {
            Log("Nemohu opravovat - není aktivní sabotáž");
            return false;
        }
        
        if (IsRepairing)
        {
            Log($"Již opravuji stanici: {RepairingStationId}");
            return false;
        }
        
        var station = RepairStations.FirstOrDefault(s => s.StationId == stationId);
        if (station == null)
        {
            Log($"Opravná stanice {stationId} neexistuje");
            return false;
        }
        
        var distance = Position.DistanceTo(station.Location);
        if (distance > 5.0)
        {
            Log($"Opravná stanice {stationId} je příliš daleko: {distance:F1}m");
            return false;
        }
        
        Log($">>> ZAČÍNÁM OPRAVU stanice: {stationId}");
        _client.Send(new ActivateRepairStation { StationId = stationId });
        return true;
    }
    
    /// <summary>
    /// Stop repairing current station
    /// </summary>
    public bool TryStopRepair()
    {
        if (!IsRepairing || RepairingStationId == null)
        {
            Log("Nejsem u opravné stanice");
            return false;
        }
        
        Log($">>> UKONČUJI OPRAVU stanice: {RepairingStationId}");
        _client.Send(new DeactivateRepairStation { StationId = RepairingStationId });
        return true;
    }
    
    /// <summary>
    /// Find nearest repair station for current sabotage
    /// </summary>
    public RepairStationInfo? FindNearestRepairStation(double maxDistance = double.MaxValue)
    {
        if (!SabotageActive) return null;
        
        RepairStationInfo? nearest = null;
        double nearestDist = maxDistance;
        
        foreach (var station in RepairStations)
        {
            if (station.IsRepaired) continue;
            
            var dist = Position.DistanceTo(station.Location);
            if (dist < nearestDist)
            {
                nearestDist = dist;
                nearest = station;
            }
        }
        
        return nearest;
    }
    
    /// <summary>
    /// Move to nearest repair station
    /// </summary>
    public bool MoveTowardsNearestRepairStation(double speed = 1.0)
    {
        var station = FindNearestRepairStation();
        if (station == null) return false;
        
        MoveTowards(station.Location, speed);
        return true;
    }
    
    /// <summary>
    /// Check if at repair station and can start repair
    /// </summary>
    public bool IsAtRepairStation(string stationId, double maxDistance = 5.0)
    {
        var station = RepairStations.FirstOrDefault(s => s.StationId == stationId);
        if (station == null) return false;
        
        return Position.DistanceTo(station.Location) <= maxDistance;
    }
    
    /// <summary>
    /// Automatic repair: find nearest station, move to it, and start repair
    /// </summary>
    public bool TryAutoRepair()
    {
        if (!SabotageActive) return false;
        if (IsRepairing) return false;
        
        var station = FindNearestRepairStation(5.0);
        if (station != null && !station.IsRepaired)
        {
            return TryStartRepair(station.StationId);
        }
        
        return false;
    }
    
    #endregion
    
    #region Autonomous Simulation
    
    public void StartSimulation()
    {
        if (_simulationTask != null) return;
        
        _cts = new CancellationTokenSource();
        _simulationTask = Task.Run(() => SimulationLoopAsync(_cts.Token));
        Log("Simulace spuštěna");
    }
    
    public void StopSimulation()
    {
        if (_cts == null) return;
        
        _cts.Cancel();
        try { _simulationTask?.Wait(1000); } catch { }
        _simulationTask = null;
        _cts = null;
        Log("Simulace zastavena");
    }
    
    private async Task SimulationLoopAsync(CancellationToken ct)
    {
        var center = LobbyState?.PlayAreaCenter ?? new Position(50.0, 14.0);
        var radius = LobbyState?.PlayAreaRadius ?? 500;
        
        MoveTo(center);
        
        while (!ct.IsCancellationRequested && !GameEnded)
        {
            try
            {
                Update();
                
                // In meeting - handle voting
                if (InMeeting)
                {
                    // First move to meeting location
                    if (MeetingLocation.HasValue && IsAlive)
                    {
                        var meetLoc = MeetingLocation.Value;
                        var distToMeeting = Position.DistanceTo(meetLoc);
                        if (distToMeeting > 5)
                        {
                            MoveTowards(meetLoc, 20);
                            await Task.Delay(200, ct);
                            continue;
                        }
                    }
                    
                    if (IsAlive && !HasVotedThisMeeting)
                    {
                        // Wait a bit before voting (simulate discussion)
                        await Task.Delay(500 + _random.Next(1500), ct);
                        Update();
                        
                        if (InMeeting && !HasVotedThisMeeting)
                        {
                            TryVoteSmart();
                        }
                    }
                    
                    // Wait for meeting to end
                    await Task.Delay(300, ct);
                    continue;
                }
                
                // Impostor logic
                if (IsImpostor && IsAlive)
                {
                    await ImpostorActionAsync(center, radius, ct);
                }
                // Alive Crew logic
                else if (IsCrew && IsAlive)
                {
                    await CrewActionAsync(center, radius, ct);
                }
                // Dead player (ghost) - can still do tasks
                else if (IsCrew && IsDead)
                {
                    await GhostTaskActionAsync(center, radius, ct);
                }
                // Dead impostor - just watch
                else if (IsImpostor && IsDead)
                {
                    await Task.Delay(1000, ct);
                }
                
                await Task.Delay(300, ct);
            }
            catch (OperationCanceledException)
            {
                break;
            }
            catch (Exception ex)
            {
                Log($"Simulation error: {ex.Message}");
                await Task.Delay(1000, ct);
            }
        }
        
        Log("Simulace dokončena");
    }
    
    private async Task ImpostorActionAsync(Position center, double radius, CancellationToken ct)
    {
        // Try to kill nearby player
        var target = FindKillTarget(8.0);
        if (target != null)
        {
            // Higher chance to kill if alone with victim
            var nearbyCount = NearbyPlayers.Values.Count(p => 
                p.State == PlayerState.Alive && 
                p.ClientUuid != ClientUuid && 
                Position.DistanceTo(p.Position) < 20);
            
            var killChance = nearbyCount <= 1 ? 0.9 : 0.3;
            
            if (_random.NextDouble() < killChance)
            {
                TryKill(target);
                await Task.Delay(300, ct);
                
                // Move away from body
                var escapePos = GetRandomPositionNear(Position, 30);
                MoveTowards(escapePos, 15);
                await Task.Delay(500, ct);
                return;
            }
        }
        
        // Wander around looking for isolated targets
        var wanderPos = GetRandomPositionNear(center, radius * 0.6);
        MoveTowards(wanderPos, 5);
    }
    
    private async Task CrewActionAsync(Position center, double radius, CancellationToken ct)
    {
        // Priority 1: Report nearby bodies
        var body = FindNearbyBody(8.0);
        if (body != null)
        {
            Log($"NAŠEL JSEM TĚLO: {body.BodyId}");
            // Move closer if needed
            if (Position.DistanceTo(body.Location) > 3)
            {
                MoveTowards(body.Location, 3);
                await Task.Delay(200, ct);
            }
            TryReportBody(body.BodyId);
            await Task.Delay(500, ct);
            return;
        }
        
        // Priority 2: Do tasks (instant completion)
        var nearbyTask = FindNearbyTask(5.0);
        if (nearbyTask != null)
        {
            TryCompleteTask(nearbyTask.TaskId);
            await Task.Delay(500, ct);
            return;
        }
        
        // Priority 3: Move to next task
        var nextTask = GetNextIncompleteTask();
        if (nextTask != null)
        {
            MoveTowards(nextTask.Location, 5);
        }
        else
        {
            // All tasks done, wander
            var wanderPos = GetRandomPositionNear(center, radius * 0.5);
            MoveTowards(wanderPos, 3);
        }
    }
    
    private async Task GhostTaskActionAsync(Position center, double radius, CancellationToken ct)
    {
        // Ghosts can complete tasks faster (no danger)
        var nearbyTask = FindNearbyTask(10.0);
        if (nearbyTask != null)
        {
            TryCompleteTask(nearbyTask.TaskId);
            await Task.Delay(100, ct);
            return;
        }
        
        // Move to next task
        var nextTask = GetNextIncompleteTask();
        if (nextTask != null)
        {
            // Ghosts move much faster
            MoveTowards(nextTask.Location, 20);
        }
    }
    
    #endregion
    
    #region Logging
    
    private void Log(string message)
    {
        OnLog?.Invoke(message);
    }
    
    private void LogError(string message)
    {
        LastError = message;
        OnError?.Invoke(message);
        Log($"ERROR: {message}");
    }
    
    #endregion
    
    #region Stats
    
    public string GetStats()
    {
        return $"[{DisplayName}] Role={Role}, State={State}, Kills={TotalKills}, Tasks={TasksCompleted}/{MyTasks.Count}, " +
               $"Reports={BodiesReported}, Votes={VotesCast}, Meetings={MeetingsAttended}, " +
               $"Killed={WasKilled}, Ejected={WasEjected}";
    }
    
    public void PrintDetailedStats()
    {
        Log("========== DETAILED STATS ==========");
        Log($"  Display Name: {DisplayName}");
        Log($"  Client UUID: {ClientUuid}");
        Log($"  Role: {Role}");
        Log($"  Final State: {State}");
        Log($"  --- Actions ---");
        Log($"  Kills Attempted: {TotalKills}");
        Log($"  Tasks Completed: {TasksCompleted}/{MyTasks.Count}");
        Log($"  Bodies Reported: {BodiesReported}");
        Log($"  Votes Cast: {VotesCast}");
        Log($"  Meetings Attended: {MeetingsAttended}");
        Log($"  --- Fate ---");
        Log($"  Was Killed: {WasKilled}");
        Log($"  Was Ejected: {WasEjected}");
        Log($"  --- Game Result ---");
        Log($"  Winning Faction: {WinningFaction}");
        Log($"  Result: {GameResult}");
        Log("=====================================");
    }
    
    #endregion
    
    public void Dispose()
    {
        StopSimulation();
        _client.Dispose();
    }
    
    #region Map Data Access
    
    /// <summary>
    /// Get the map data payload from the current lobby state (if available)
    /// </summary>
    public MapDataPayload? MapData => _client.CurrentLobbyState?.MapData;
    
    /// <summary>
    /// Check if map data is available for the current lobby
    /// </summary>
    public bool HasMapData => MapData != null;
    
    /// <summary>
    /// Check if map data loading is complete (true if loaded or Overpass disabled)
    /// </summary>
    public bool IsMapDataReady => _client.CurrentLobbyState?.MapDataReady ?? false;
    
    /// <summary>
    /// Get the play area center from lobby state
    /// </summary>
    public Position? PlayAreaCenter => _client.CurrentLobbyState?.PlayAreaCenter;
    
    /// <summary>
    /// Get the play area radius from lobby state
    /// </summary>
    public double PlayAreaRadius => _client.CurrentLobbyState?.PlayAreaRadius ?? 500;
    
    #endregion
}

/// <summary>
/// Comprehensive test suite for Overpass API and reachability testing
/// </summary>
public class OverpassApiTests
{
    private readonly string _serverHost;
    private readonly int _serverPort;
    private readonly Action<string>? _logger;
    
    public OverpassApiTests(string serverHost = "localhost", int serverPort = 7777, Action<string>? logger = null)
    {
        _serverHost = serverHost;
        _serverPort = serverPort;
        _logger = logger;
    }
    
    private void Log(string message)
    {
        _logger?.Invoke(message);
        Console.WriteLine(message);
    }
    
    /// <summary>
    /// Run all Overpass API tests
    /// </summary>
    public async Task<OverpassTestResults> RunAllTestsAsync(Position testCenter, double testRadius = 500)
    {
        var results = new OverpassTestResults();
        
        Log("═══════════════════════════════════════════════════════════════");
        Log("           OVERPASS API & REACHABILITY TEST SUITE");
        Log("═══════════════════════════════════════════════════════════════");
        Log($"Test Center: {testCenter.Lat:F6}, {testCenter.Lon:F6}");
        Log($"Test Radius: {testRadius}m");
        Log("");
        
        // Test 1: Create lobby and verify map data is fetched
        Log("TEST 1: Map Data Fetch on Lobby Creation");
        Log("─────────────────────────────────────────");
        results.MapDataFetchTest = await TestMapDataFetchAsync(testCenter, testRadius);
        LogTestResult("Map Data Fetch", results.MapDataFetchTest);
        await Task.Delay(1000); // Delay between tests
        
        // Test 2: Verify map data structure
        Log("");
        Log("TEST 2: Map Data Structure Validation");
        Log("─────────────────────────────────────────");
        results.MapDataStructureTest = await TestMapDataStructureAsync(testCenter, testRadius);
        LogTestResult("Map Data Structure", results.MapDataStructureTest);
        await Task.Delay(1000); // Delay between tests
        
        // Test 3: Verify task positions are on reachable paths
        Log("");
        Log("TEST 3: Task Position Reachability");
        Log("─────────────────────────────────────────");
        results.TaskReachabilityTest = await TestTaskReachabilityAsync(testCenter, testRadius);
        LogTestResult("Task Reachability", results.TaskReachabilityTest);
        await Task.Delay(1000); // Delay between tests
        
        // Test 4: Verify repair station positions are reachable
        Log("");
        Log("TEST 4: Repair Station Reachability (Sabotage)");
        Log("─────────────────────────────────────────");
        results.RepairStationReachabilityTest = await TestRepairStationReachabilityAsync(testCenter, testRadius);
        LogTestResult("Repair Station Reachability", results.RepairStationReachabilityTest);
        await Task.Delay(1000); // Delay between tests
        
        // Test 5: Verify positions are within play area
        Log("");
        Log("TEST 5: Play Area Boundary Validation");
        Log("─────────────────────────────────────────");
        results.PlayAreaBoundaryTest = await TestPlayAreaBoundaryAsync(testCenter, testRadius);
        LogTestResult("Play Area Boundary", results.PlayAreaBoundaryTest);
        await Task.Delay(1000); // Delay between tests
        
        // Test 6: Test multiple lobby creations for consistency
        Log("");
        Log("TEST 6: Map Data Consistency Across Lobbies");
        Log("─────────────────────────────────────────");
        results.ConsistencyTest = await TestMapDataConsistencyAsync(testCenter, testRadius);
        LogTestResult("Map Data Consistency", results.ConsistencyTest);
        
        // Summary
        Log("");
        Log("═══════════════════════════════════════════════════════════════");
        Log("                        TEST SUMMARY");
        Log("═══════════════════════════════════════════════════════════════");
        results.PrintSummary(Log);
        
        return results;
    }
    
    private void LogTestResult(string testName, TestResult result)
    {
        var status = result.Passed ? "✓ PASS" : "✗ FAIL";
        Log($"  [{status}] {testName}");
        if (!string.IsNullOrEmpty(result.Details))
        {
            foreach (var line in result.Details.Split('\n'))
            {
                Log($"      {line}");
            }
        }
        if (!result.Passed && !string.IsNullOrEmpty(result.Error))
        {
            Log($"      ERROR: {result.Error}");
        }
    }
    
    /// <summary>
    /// Wait for map data to be loaded with polling and timeout
    /// Note: Map data is now fetched when game starts, not on lobby creation
    /// </summary>
    private async Task<bool> WaitForMapDataAsync(SimulatorClient client, SimulatorClient? otherClient, int timeoutMs = 10000, int pollIntervalMs = 500)
    {
        var sw = System.Diagnostics.Stopwatch.StartNew();
        
        while (sw.ElapsedMilliseconds < timeoutMs)
        {
            // Update client to receive any pending messages
            client.Update();
            otherClient?.Update();
            
            // Check if client has map data loaded
            if (client.HasMapData)
            {
                Log($"      Map data loaded after {sw.ElapsedMilliseconds}ms");
                return true;
            }
            
            // Also check if MapDataReady indicates no data expected (Overpass disabled)
            if (client.IsMapDataReady && !client.HasMapData)
            {
                Log($"      Map data not available (Overpass may be disabled)");
                return false;
            }
            
            await Task.Delay(pollIntervalMs);
        }
        
        Log($"      Timeout waiting for map data ({timeoutMs}ms)");
        return false;
    }
    
    /// <summary>
    /// Wait for game to start after map data is received (Loading phase complete)
    /// </summary>
    private async Task<bool> WaitForGameStartAfterMapDataAsync(SimulatorClient owner, SimulatorClient player, int timeoutMs = 20000, int pollIntervalMs = 500)
    {
        var sw = System.Diagnostics.Stopwatch.StartNew();
        
        while (sw.ElapsedMilliseconds < timeoutMs)
        {
            owner.Update();
            player.Update();
            
            // Game is fully started when phase is Playing and roles are assigned
            if (owner.GamePhase == "Playing" && owner.Role.HasValue)
            {
                Log($"      Game started after {sw.ElapsedMilliseconds}ms");
                return true;
            }
            
            await Task.Delay(pollIntervalMs);
        }
        
        Log($"      Timeout waiting for game start ({timeoutMs}ms) - Phase: {owner.GamePhase}");
        return false;
    }
    
    /// <summary>
    /// Start a game and wait for map data to be loaded
    /// The new flow is: StartGame -> Loading phase -> MapData fetched -> All confirm -> Playing
    /// </summary>
    private async Task<bool> StartGameAndWaitForMapDataAsync(SimulatorClient owner, SimulatorClient player, int timeoutMs = 25000)
    {
        Log("      Starting game (this will trigger map data fetch)...");
        owner.StartGame();
        
        // Wait for map data first (update both clients so both send confirmations)
        if (!await WaitForMapDataAsync(owner, player, timeoutMs))
        {
            return false;
        }
        
        // Ensure both clients update to send MapDataReceived confirmation
        for (int i = 0; i < 10; i++)
        {
            owner.Update();
            player.Update();
            await Task.Delay(100);
        }
        
        // Wait for game to actually start (after all confirmations)
        return await WaitForGameStartAfterMapDataAsync(owner, player, 10000);
    }
    
    private async Task<TestResult> TestMapDataFetchAsync(Position center, double radius)
    {
        var result = new TestResult { TestName = "MapDataFetch" };
        SimulatorClient? owner = null;
        SimulatorClient? player = null;
        
        try
        {
            // Create owner and player (need 2 players to start game)
            owner = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "MapOwner");
            player = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "MapPlayer");
            
            // Connect owner
            Log("      Connecting owner to server...");
            if (!await owner.ConnectAsync(_serverHost, _serverPort))
            {
                result.Error = $"Failed to connect owner to server at {_serverHost}:{_serverPort}";
                return result;
            }
            
            // Create lobby with specific center
            Log($"      Creating lobby at {center.Lat:F6}, {center.Lon:F6} with radius {radius}m...");
            if (!await owner.CreateLobbyAsync(null, center, radius))
            {
                result.Error = $"Failed to create lobby: {owner.LastError ?? "Unknown error"}";
                return result;
            }
            Log($"      Lobby created: {owner.JoinCode}");
            
            // Connect player
            Log("      Connecting player to server...");
            if (!await player.ConnectAsync(_serverHost, _serverPort))
            {
                result.Error = "Failed to connect player";
                return result;
            }
            
            // Join player to lobby
            if (!await player.JoinLobbyAsync(owner.JoinCode!))
            {
                result.Error = "Failed to join player to lobby";
                return result;
            }
            
            // Start game - this triggers map data fetch
            Log("      Starting game (triggers Overpass API fetch)...");
            if (!await StartGameAndWaitForMapDataAsync(owner, player, timeoutMs: 25000))
            {
                result.Error = "Timeout waiting for map data (Overpass API may be slow or unavailable)";
                return result;
            }
            
            if (owner.HasMapData)
            {
                var mapData = owner.MapData!;
                var buildings = mapData.GetBuildings();
                var pathways = mapData.GetPathways();
                var areas = mapData.GetAreas();
                var pois = mapData.GetPOIs();
                result.Passed = true;
                result.Details = $"Buildings: {buildings.Count}\n" +
                                $"Pathways: {pathways.Count}\n" +
                                $"Areas: {areas.Count}\n" +
                                $"POIs: {pois.Count}";
            }
            else
            {
                result.Error = "Map data was not received after game start";
            }
        }
        catch (Exception ex)
        {
            result.Error = ex.Message;
        }
        finally
        {
            owner?.Dispose();
            player?.Dispose();
        }
        
        return result;
    }
    
    private async Task<TestResult> TestMapDataStructureAsync(Position center, double radius)
    {
        var result = new TestResult { TestName = "MapDataStructure" };
        SimulatorClient? owner = null;
        SimulatorClient? player = null;
        
        try
        {
            owner = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "StructOwner");
            player = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "StructPlayer");
            
            if (!await owner.ConnectAsync(_serverHost, _serverPort))
            {
                result.Error = "Failed to connect owner";
                return result;
            }
            
            if (!await owner.CreateLobbyAsync(null, center, radius))
            {
                result.Error = "Failed to create lobby";
                return result;
            }
            
            if (!await player.ConnectAsync(_serverHost, _serverPort))
            {
                result.Error = "Failed to connect player";
                return result;
            }
            
            if (!await player.JoinLobbyAsync(owner.JoinCode!))
            {
                result.Error = "Failed to join player to lobby";
                return result;
            }
            
            // Start game to trigger map data fetch
            Log("      Starting game (triggers Overpass API fetch)...");
            if (!await StartGameAndWaitForMapDataAsync(owner, player, timeoutMs: 25000))
            {
                result.Error = "Timeout waiting for map data";
                return result;
            }
            
            if (!owner.HasMapData)
            {
                result.Error = "No map data available";
                return result;
            }
            
            var mapData = owner.MapData!;
            var buildings = mapData.GetBuildings();
            var pathways = mapData.GetPathways();
            var areas = mapData.GetAreas();
            var pois = mapData.GetPOIs();
            var issues = new List<string>();
            var stats = new List<string>();
            
            // Check buildings have valid outlines
            int validBuildings = 0;
            foreach (var building in buildings)
            {
                if (building.Outline == null || building.Outline.Count < 3)
                {
                    issues.Add($"Building {building.Id}: Invalid outline (< 3 points)");
                }
                else
                {
                    validBuildings++;
                }
            }
            stats.Add($"Valid buildings: {validBuildings}/{buildings.Count}");
            
            // Check pathways have valid points
            int validPathways = 0;
            foreach (var pathway in pathways)
            {
                if (pathway.Points == null || pathway.Points.Count < 2)
                {
                    issues.Add($"Pathway {pathway.Id}: Invalid points (< 2)");
                }
                else
                {
                    validPathways++;
                }
            }
            stats.Add($"Valid pathways: {validPathways}/{pathways.Count}");
            
            // Check areas
            int validAreas = 0;
            foreach (var area in areas)
            {
                if (area.Outline == null || area.Outline.Count < 3)
                {
                    issues.Add($"Area {area.Id}: Invalid outline");
                }
                else
                {
                    validAreas++;
                }
            }
            stats.Add($"Valid areas: {validAreas}/{areas.Count}");
            
            // Check POIs have valid positions
            int validPOIs = 0;
            foreach (var poi in pois)
            {
                if (poi.Location.Lat != 0 || poi.Location.Lon != 0)
                {
                    validPOIs++;
                }
            }
            stats.Add($"Valid POIs: {validPOIs}/{pois.Count}");
            
            result.Passed = issues.Count == 0;
            result.Details = string.Join("\n", stats);
            if (issues.Count > 0)
            {
                result.Error = string.Join("; ", issues.Take(5));
            }
        }
        catch (Exception ex)
        {
            result.Error = ex.Message;
        }
        finally
        {
            owner?.Dispose();
            player?.Dispose();
        }
        
        return result;
    }
    
    private async Task<TestResult> TestTaskReachabilityAsync(Position center, double radius)
    {
        var result = new TestResult { TestName = "TaskReachability" };
        SimulatorClient? owner = null;
        SimulatorClient? player = null;
        
        try
        {
            // Create lobby owner
            owner = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "TaskOwner");
            player = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "TaskPlayer");
            
            if (!await owner.ConnectAsync(_serverHost, _serverPort))
            {
                result.Error = "Owner failed to connect";
                return result;
            }
            
            if (!await owner.CreateLobbyAsync(null, center, radius, 1, 5))
            {
                result.Error = "Failed to create lobby";
                return result;
            }
            
            var joinCode = owner.JoinCode;
            
            if (!await player.ConnectAsync(_serverHost, _serverPort))
            {
                result.Error = "Player failed to connect";
                return result;
            }
            
            if (!await player.JoinLobbyAsync(joinCode!))
            {
                result.Error = "Player failed to join";
                return result;
            }
            
            // Start game - this triggers map data fetch
            Log("      Starting game (triggers Overpass API fetch)...");
            if (!await StartGameAndWaitForMapDataAsync(owner, player, timeoutMs: 25000))
            {
                result.Error = "Timeout waiting for game to start";
                return result;
            }
            
            // Check task positions
            var mapData = owner.MapData;
            var tasks = owner.MyTasks.Count > 0 ? owner.MyTasks : player.MyTasks;
            
            if (tasks.Count == 0)
            {
                result.Error = "No tasks were assigned";
                return result;
            }
            
            int tasksInPlayArea = 0;
            int tasksOnPathways = 0;
            var details = new List<string>();
            
            foreach (var task in tasks)
            {
                var distFromCenter = task.Location.DistanceTo(center);
                bool inPlayArea = distFromCenter <= radius;
                if (inPlayArea) tasksInPlayArea++;
                
                // Check if task is near any pathway
                bool nearPathway = false;
                if (mapData != null)
                {
                    var pathways = mapData.GetPathways();
                    foreach (var pathway in pathways)
                    {
                        foreach (var point in pathway.Points)
                        {
                            if (task.Location.DistanceTo(point) < 15) // Within 15m of pathway
                            {
                                nearPathway = true;
                                break;
                            }
                        }
                        if (nearPathway) break;
                    }
                }
                if (nearPathway) tasksOnPathways++;
                
                details.Add($"{task.Name}: {distFromCenter:F0}m from center, {(inPlayArea ? "in" : "OUT OF")} play area, {(nearPathway ? "near" : "NOT NEAR")} pathway");
            }
            
            result.Passed = tasksInPlayArea == tasks.Count;
            result.Details = $"Tasks in play area: {tasksInPlayArea}/{tasks.Count}\n" +
                            $"Tasks near pathways: {tasksOnPathways}/{tasks.Count}\n" +
                            string.Join("\n", details.Take(3));
            
            if (tasksInPlayArea < tasks.Count)
            {
                result.Error = $"{tasks.Count - tasksInPlayArea} tasks are outside play area!";
            }
        }
        catch (Exception ex)
        {
            result.Error = ex.Message;
        }
        finally
        {
            owner?.Dispose();
            player?.Dispose();
        }
        
        return result;
    }
    
    private async Task<TestResult> TestRepairStationReachabilityAsync(Position center, double radius)
    {
        var result = new TestResult { TestName = "RepairStationReachability" };
        SimulatorClient? owner = null;
        SimulatorClient? player = null;
        
        try
        {
            // Create lobby with 2 players
            owner = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "SabOwner");
            player = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "SabPlayer");
            
            if (!await owner.ConnectAsync(_serverHost, _serverPort)) { result.Error = "Owner connect failed"; return result; }
            if (!await owner.CreateLobbyAsync(null, center, radius, 1, 3)) { result.Error = "Create lobby failed"; return result; }
            
            var joinCode = owner.JoinCode;
            
            if (!await player.ConnectAsync(_serverHost, _serverPort)) { result.Error = "Player connect failed"; return result; }
            if (!await player.JoinLobbyAsync(joinCode!)) { result.Error = "Join lobby failed"; return result; }
            
            // Start game - this triggers map data fetch
            Log("      Starting game (triggers Overpass API fetch)...");
            if (!await StartGameAndWaitForMapDataAsync(owner, player, timeoutMs: 25000))
            {
                result.Error = "Timeout waiting for game to start";
                return result;
            }
            
            // Find impostor and trigger sabotage
            var impostor = owner.IsImpostor ? owner : (player.IsImpostor ? player : null);
            if (impostor == null)
            {
                result.Error = "No impostor found";
                return result;
            }
            
            // Wait for sabotage cooldown
            await Task.Delay(1000);
            
            // Try to start a sabotage
            impostor.TrySabotage(SabotageType.CriticalMeltdown);
            await Task.Delay(1500);
            
            // Check repair station positions
            var crew = owner.IsCrew ? owner : player;
            var stations = crew.RepairStations;
            
            if (stations.Count == 0)
            {
                // Sabotage might have been blocked, try CommsBlackout
                impostor.TrySabotage(SabotageType.CommsBlackout);
                await Task.Delay(1000);
                stations = crew.RepairStations;
            }
            
            if (stations.Count == 0)
            {
                result.Passed = true; // No sabotage was possible, but that's OK
                result.Details = "Sabotage cooldown active, skipping repair station test";
                return result;
            }
            
            int stationsInPlayArea = 0;
            var details = new List<string>();
            
            foreach (var station in stations)
            {
                var distFromCenter = station.Location.DistanceTo(center);
                bool inPlayArea = distFromCenter <= radius;
                if (inPlayArea) stationsInPlayArea++;
                
                details.Add($"{station.Name}: {distFromCenter:F0}m from center ({(inPlayArea ? "OK" : "OUT!")})");
            }
            
            result.Passed = stationsInPlayArea == stations.Count;
            result.Details = $"Stations in play area: {stationsInPlayArea}/{stations.Count}\n" +
                            string.Join("\n", details);
            
            if (stationsInPlayArea < stations.Count)
            {
                result.Error = $"{stations.Count - stationsInPlayArea} repair stations are outside play area!";
            }
        }
        catch (Exception ex)
        {
            result.Error = ex.Message;
        }
        finally
        {
            owner?.Dispose();
            player?.Dispose();
        }
        
        return result;
    }
    
    private async Task<TestResult> TestPlayAreaBoundaryAsync(Position center, double radius)
    {
        var result = new TestResult { TestName = "PlayAreaBoundary" };
        SimulatorClient? owner = null;
        SimulatorClient? player = null;
        
        try
        {
            owner = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "BoundOwner");
            player = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), "BoundPlayer");
            
            if (!await owner.ConnectAsync(_serverHost, _serverPort)) { result.Error = "Connect failed"; return result; }
            if (!await owner.CreateLobbyAsync(null, center, radius)) { result.Error = "Create lobby failed"; return result; }
            
            if (!await player.ConnectAsync(_serverHost, _serverPort)) { result.Error = "Player connect failed"; return result; }
            if (!await player.JoinLobbyAsync(owner.JoinCode!)) { result.Error = "Join lobby failed"; return result; }
            
            // Start game - this triggers map data fetch
            Log("      Starting game (triggers Overpass API fetch)...");
            if (!await StartGameAndWaitForMapDataAsync(owner, player, timeoutMs: 25000))
            {
                result.Passed = true;
                result.Details = "No map data to validate (Overpass might be disabled or timed out)";
                return result;
            }
            
            if (!owner.HasMapData)
            {
                result.Passed = true;
                result.Details = "No map data to validate (Overpass might be disabled)";
                return result;
            }
            
            var mapData = owner.MapData!;
            var pathways = mapData.GetPathways();
            var pois = mapData.GetPOIs();
            int outsidePathwayPoints = 0;
            int totalPathwayPoints = 0;
            int outsidePOIs = 0;
            int totalPOIs = pois.Count;
            
            // Check all pathway points
            foreach (var pathway in pathways)
            {
                foreach (var point in pathway.Points)
                {
                    totalPathwayPoints++;
                    if (point.DistanceTo(center) > radius * 1.1) // Allow 10% margin
                    {
                        outsidePathwayPoints++;
                    }
                }
            }
            
            // Check all POIs
            foreach (var poi in pois)
            {
                if (poi.Location.DistanceTo(center) > radius * 1.1)
                {
                    outsidePOIs++;
                }
            }
            
            // Allow some points outside (Overpass query might include slightly outside data)
            double pathwayOutsidePercent = totalPathwayPoints > 0 ? (outsidePathwayPoints * 100.0 / totalPathwayPoints) : 0;
            double poiOutsidePercent = totalPOIs > 0 ? (outsidePOIs * 100.0 / totalPOIs) : 0;
            
            result.Passed = pathwayOutsidePercent < 20 && poiOutsidePercent < 20;
            result.Details = $"Pathway points outside boundary: {outsidePathwayPoints}/{totalPathwayPoints} ({pathwayOutsidePercent:F1}%)\n" +
                            $"POIs outside boundary: {outsidePOIs}/{totalPOIs} ({poiOutsidePercent:F1}%)";
            
            if (!result.Passed)
            {
                result.Error = "Too many map elements outside play area boundary";
            }
        }
        catch (Exception ex)
        {
            result.Error = ex.Message;
        }
        finally
        {
            owner?.Dispose();
            player?.Dispose();
        }
        
        return result;
    }
    
    private async Task<TestResult> TestMapDataConsistencyAsync(Position center, double radius)
    {
        var result = new TestResult { TestName = "MapDataConsistency" };
        var owners = new List<SimulatorClient>();
        var players = new List<SimulatorClient>();
        
        try
        {
            // Create 3 lobbies at the same location and compare map data
            var mapDataResults = new List<(int buildings, int pathways, int pois)>();
            
            for (int i = 0; i < 3; i++)
            {
                Log($"      Creating lobby {i + 1}/3...");
                var owner = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), $"ConsOwner{i}");
                var player = new SimulatorClient(Guid.NewGuid().ToString("N").Substring(0, 8), $"ConsPlayer{i}");
                owners.Add(owner);
                players.Add(player);
                
                if (!await owner.ConnectAsync(_serverHost, _serverPort)) continue;
                if (!await owner.CreateLobbyAsync(null, center, radius)) continue;
                
                if (!await player.ConnectAsync(_serverHost, _serverPort)) continue;
                if (!await player.JoinLobbyAsync(owner.JoinCode!)) continue;
                
                // Start game to trigger map data fetch
                if (await StartGameAndWaitForMapDataAsync(owner, player, timeoutMs: 25000))
                {
                    if (owner.HasMapData)
                    {
                        var md = owner.MapData!;
                        mapDataResults.Add((md.GetBuildings().Count, md.GetPathways().Count, md.GetPOIs().Count));
                    }
                }
                
                await Task.Delay(300);
            }
            
            if (mapDataResults.Count < 2)
            {
                result.Passed = true;
                result.Details = "Not enough successful map data fetches to compare consistency";
                return result;
            }
            
            // Check if all results are the same (should be cached)
            var first = mapDataResults[0];
            bool allSame = mapDataResults.All(r => r == first);
            
            result.Passed = allSame;
            result.Details = string.Join("\n", mapDataResults.Select((r, i) => 
                $"Lobby {i + 1}: {r.buildings} buildings, {r.pathways} pathways, {r.pois} POIs"));
            
            if (!allSame)
            {
                result.Error = "Map data varies between lobbies at same location (caching issue?)";
            }
        }
        catch (Exception ex)
        {
            result.Error = ex.Message;
        }
        finally
        {
            foreach (var o in owners) o?.Dispose();
            foreach (var p in players) p?.Dispose();
        }
        
        return result;
    }
}

/// <summary>
/// Results from a single test
/// </summary>
public class TestResult
{
    public string TestName { get; set; } = "";
    public bool Passed { get; set; }
    public string? Details { get; set; }
    public string? Error { get; set; }
}

/// <summary>
/// Aggregate results from all Overpass tests
/// </summary>
public class OverpassTestResults
{
    public TestResult MapDataFetchTest { get; set; } = new();
    public TestResult MapDataStructureTest { get; set; } = new();
    public TestResult TaskReachabilityTest { get; set; } = new();
    public TestResult RepairStationReachabilityTest { get; set; } = new();
    public TestResult PlayAreaBoundaryTest { get; set; } = new();
    public TestResult ConsistencyTest { get; set; } = new();
    
    public int TotalTests => 6;
    public int PassedTests => new[] { MapDataFetchTest, MapDataStructureTest, TaskReachabilityTest, 
        RepairStationReachabilityTest, PlayAreaBoundaryTest, ConsistencyTest }.Count(t => t.Passed);
    public int FailedTests => TotalTests - PassedTests;
    public bool AllPassed => PassedTests == TotalTests;
    
    public void PrintSummary(Action<string> log)
    {
        log($"  Total Tests: {TotalTests}");
        log($"  Passed: {PassedTests}");
        log($"  Failed: {FailedTests}");
        log("");
        
        if (AllPassed)
        {
            log("  ✓ ALL OVERPASS TESTS PASSED!");
        }
        else
        {
            log("  ✗ SOME TESTS FAILED:");
            if (!MapDataFetchTest.Passed) log($"    - Map Data Fetch: {MapDataFetchTest.Error}");
            if (!MapDataStructureTest.Passed) log($"    - Map Data Structure: {MapDataStructureTest.Error}");
            if (!TaskReachabilityTest.Passed) log($"    - Task Reachability: {TaskReachabilityTest.Error}");
            if (!RepairStationReachabilityTest.Passed) log($"    - Repair Station Reachability: {RepairStationReachabilityTest.Error}");
            if (!PlayAreaBoundaryTest.Passed) log($"    - Play Area Boundary: {PlayAreaBoundaryTest.Error}");
            if (!ConsistencyTest.Passed) log($"    - Consistency: {ConsistencyTest.Error}");
        }
    }
}
}
