using System.Net;
using System.Net.Http.Json;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using StackExchange.Redis;

[Collection("Integration")]
public class SimulationRunnerTests : IAsyncLifetime
{
    private readonly ApiFixture _fixture;
    private ISimulationRunner _runner = null!;
    private TestSimulationClientFactory _clientFactory = null!;

    public SimulationRunnerTests(ApiFixture fixture) => _fixture = fixture;

    public async Task InitializeAsync()
    {
        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var redis = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>();
        await DbResetHelper.ResetAsync(db);
        await DataSeeder.SeedThresholdsOnlyAsync(db, redis);

        _runner = _fixture.Services.GetRequiredService<ISimulationRunner>();
        _clientFactory = _fixture.SimulationClientFactory;
        _clientFactory.HangOnCreate = false;
        _clientFactory.FailOnCreate = false;

        foreach (var run in _runner.ListRuns())
            _runner.Cancel(run.RunId);
    }

    public Task DisposeAsync()
    {
        _clientFactory.HangOnCreate = false;
        _clientFactory.FailOnCreate = false;
        return Task.CompletedTask;
    }

    [Fact]
    public async Task Start_UnknownScenario_Returns422()
    {
        var act = () => _runner.StartAsync("does-not-exist", 60, "tester", CancellationToken.None);

        var ex = await act.Should().ThrowAsync<ValidationException>();
        ex.Which.ErrorCode.Should().Be("SIMULATION_SCENARIO_UNKNOWN");

        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        (await db.SimulationRuns.CountAsync()).Should().Be(0);
    }

    [Fact]
    public async Task Start_SpeedAboveMax_Returns422()
    {
        var act = () => _runner.StartAsync("minimal-sim-01", 601, "tester", CancellationToken.None);

        var ex = await act.Should().ThrowAsync<ValidationException>();
        ex.Which.ErrorCode.Should().Be("SIMULATION_SPEED_INVALID");
    }

    [Fact]
    public async Task Start_AtConcurrencyLimit_Returns409()
    {
        _clientFactory.HangOnCreate = true;

        await _runner.StartAsync("minimal-sim-01", 60, "tester", CancellationToken.None);
        await _runner.StartAsync("minimal-sim-01", 60, "tester", CancellationToken.None);

        var act = () => _runner.StartAsync("minimal-sim-01", 60, "tester", CancellationToken.None);
        var ex = await act.Should().ThrowAsync<ConflictException>();
        ex.Which.ErrorCode.Should().Be("SIMULATION_CONCURRENCY_LIMIT");

        foreach (var run in _runner.ListRuns())
            _runner.Cancel(run.RunId);

        await WaitForAsync(() => _runner.ListRuns().All(r =>
            r.Status is SimulationRunStatus.Cancelled or SimulationRunStatus.Failed
                or SimulationRunStatus.Completed));
    }

    [Fact]
    public async Task Start_CreatesPatientMarkedSimulated()
    {
        var state = await _runner.StartAsync("minimal-sim-01", 600, "tester", CancellationToken.None);
        var completed = await WaitForRunAsync(state.RunId, SimulationRunStatus.Completed);

        completed.PatientId.Should().NotBeNull();

        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var patient = await db.Patients.SingleAsync(p => p.Id == completed.PatientId);
        patient.IsSimulated.Should().BeTrue();
    }

    [Fact]
    public async Task Run_ProgressAdvances()
    {
        var state = await _runner.StartAsync("minimal-sim-01", 600, "tester", CancellationToken.None);

        await WaitForAsync(() =>
        {
            var current = _runner.GetRun(state.RunId);
            return current is not null && current.LastOffsetMinutes > 0;
        });

        var mid = _runner.GetRun(state.RunId)!;
        mid.LastOffsetMinutes.Should().BeGreaterThan(0);

        var completed = await WaitForRunAsync(state.RunId, SimulationRunStatus.Completed);
        completed.ProgressPercent.Should().Be(100);
        completed.ObservationsSent.Should().Be(4);
    }

    [Fact]
    public async Task Stop_CancelsRun_StatusCancelled()
    {
        _clientFactory.HangOnCreate = true;
        var state = await _runner.StartAsync("minimal-sim-01", 60, "tester", CancellationToken.None);

        _runner.Cancel(state.RunId).Should().BeTrue();
        var cancelled = await WaitForRunAsync(state.RunId, SimulationRunStatus.Cancelled);
        cancelled.Status.Should().Be(SimulationRunStatus.Cancelled);

        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        (await db.Observations.CountAsync()).Should().Be(0);
    }

    [Fact]
    public async Task Stop_AlreadyCompleted_IsNoOpSuccess()
    {
        var state = await _runner.StartAsync("minimal-sim-01", 600, "tester", CancellationToken.None);
        await WaitForRunAsync(state.RunId, SimulationRunStatus.Completed);

        _runner.Cancel(state.RunId).Should().BeTrue();
        var after = _runner.GetRun(state.RunId)!;
        after.Status.Should().Be(SimulationRunStatus.Completed);
    }

    [Fact]
    public async Task Run_Failure_RecordsFailureReason()
    {
        _clientFactory.FailOnCreate = true;
        var state = await _runner.StartAsync("minimal-sim-01", 600, "tester", CancellationToken.None);

        var failed = await WaitForRunAsync(state.RunId, SimulationRunStatus.Failed);
        failed.FailureReason.Should().NotBeNullOrWhiteSpace();
        failed.FailureReason.Should().Contain("Login failed");
    }

    [Fact]
    public async Task Shutdown_CancelsActiveRuns()
    {
        _clientFactory.HangOnCreate = true;
        var state = await _runner.StartAsync("minimal-sim-01", 60, "tester", CancellationToken.None);

        var hosted = (IHostedService)_fixture.Services.GetRequiredService<SimulationRunner>();
        await hosted.StopAsync(CancellationToken.None);

        var after = await WaitForRunAsync(state.RunId, SimulationRunStatus.Cancelled);
        after.Status.Should().Be(SimulationRunStatus.Cancelled);
    }

    private async Task<SimulationRunState> WaitForRunAsync(
        Guid runId, SimulationRunStatus expected, TimeSpan? timeout = null)
    {
        try
        {
            await WaitForAsync(() => _runner.GetRun(runId)?.Status == expected, timeout);
        }
        catch (TimeoutException)
        {
            var actual = _runner.GetRun(runId);
            throw new TimeoutException(
                $"Expected run {runId} status {expected}, but was {actual?.Status}. " +
                $"FailureReason={actual?.FailureReason}");
        }

        return _runner.GetRun(runId)!;
    }

    private static async Task WaitForAsync(Func<bool> condition, TimeSpan? timeout = null)
    {
        var deadline = DateTimeOffset.UtcNow + (timeout ?? TimeSpan.FromSeconds(15));
        while (DateTimeOffset.UtcNow < deadline)
        {
            if (condition())
                return;
            await Task.Delay(25);
        }

        throw new TimeoutException("Condition was not met within the timeout.");
    }
}
