using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;

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

    public SimulationSessionTests(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);

        await WaitForAsync(() => !_runner.HasActiveRuns());
        _runner.ClearRegistry();
    }

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

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

        var session = await _runner.StartSessionAsync(
            "session-test-two", 60, "tester", CancellationToken.None);

        session.RunIds.Should().HaveCount(2);
        _runner.ListRuns().Count(r =>
                r.Status is SimulationRunStatus.Pending or SimulationRunStatus.Running)
            .Should().Be(2);

        foreach (var runId in session.RunIds)
            _runner.Cancel(runId);

        await WaitForAsync(() => !_runner.HasActiveRuns());
    }

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

        var act = () => _runner.StartSessionAsync(
            "session-test-three", 60, "tester", CancellationToken.None);

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

        _runner.ListRuns().Should().BeEmpty();

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

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

        var session = await _runner.StartSessionAsync(
            "session-test-one", null, "tester", CancellationToken.None);

        session.SessionId.Should().Be("session-test-one");
        var run = _runner.GetRun(session.RunIds[0])!;
        run.SessionId.Should().Be("session-test-one");
        run.Speed.Should().Be(120); // defaultSpeed from fixture manifest

        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var row = await db.SimulationRuns.SingleAsync(r => r.Id == run.RunId);
        row.SessionId.Should().Be("session-test-one");

        _runner.Cancel(run.RunId);
        await WaitForAsync(() => !_runner.HasActiveRuns());
    }

    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.");
    }
}
