using ReactorMaintenance.Simulation.Difficulties;
using ReactorMaintenance.Simulation.Effects;
using ReactorMaintenance.Simulation.Hazards;

namespace ReactorMaintenance.Simulation.Tests;

public sealed class SimulationEngineTests
{
    [Fact]
    public void FuelLeakNearPoweredGeneratorCreatesIgnitionForecast()
    {
        var level = LevelState.Create("Fuel leak", 6, 6)
                              .SetCell(new(2, 2), new() {
                                  Prop = ECellProp.Generator,
                                  Pipe = EPipeMedium.Fuel,
                                  LeakRate = Balancing.Current.FuelVaporFireThreshold,
                                  Pressure = Balancing.Current.PressurizedFuelLeakPressureThreshold + Balancing.Current.NeighborDistance,
                                  Integrity = Balancing.Current.DefaultEditedPipeIntegrity,
                                  Powered = true
                              });

        var forecasts = m_Engine.Forecast(level);

        Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Ignition && forecast.Position == new GridPosition(2, 2) && forecast.Turns == 1);
    }

    [Fact]
    public void CoolantLeakOnPoweredCellRaisesElectricalCharge()
    {
        var level = LevelState.Create("Wet cable", 6, 6)
                              .SetCell(new(3, 3), new() {
                                  Pipe = EPipeMedium.Coolant,
                                  LeakRate = Balancing.Current.ElectrifiedCoolantPoolingThreshold,
                                  Powered = true
                              });

        var next = m_Engine.AdvanceTurn(level);

        Assert.True(next.GetCell(new(3, 3)).Hazards.ElectricalCharge >= Balancing.Current.ElectricalChargeIncrease);
    }

    [Fact]
    public void ActiveFireSpreadsSmokeToOpenNeighbors()
    {
        var level = LevelState.Create("Smoke", 6, 6)
                              .SetCell(new(2, 2), new() {
                                  Hazards = new() {
                                      Fire = true,
                                      Smoke = Balancing.Current.SmokeSpreadThreshold
                                  }
                              });

        var next = m_Engine.AdvanceTurn(level);

        Assert.True(next.GetCell(new(2, 3)).Hazards.Smoke > 0);
    }

    [Fact]
    public void AdvanceTurnRunsConfiguredCellEffects()
    {
        var engine = new SimulationEngine([new TestCellEffect()], [], []);
        var level = LevelState.Create("Custom effect", 6, 6)
                              .SetCell(new(2, 2), new() {
                                  Hazards = new() { Heat = 1 }
                              });

        var next = engine.AdvanceTurn(level);

        Assert.Equal(5, next.GetCell(new(2, 2)).Hazards.Heat);
    }

    [Fact]
    public void AdvanceTurnRunsConfiguredAreaEffects()
    {
        var engine = new SimulationEngine([], [new TestAreaEffect()], []);
        var level = LevelState.Create("Custom area effect", 6, 6);

        var next = engine.AdvanceTurn(level);

        Assert.Equal(7, next.GetCell(new(2, 2)).Hazards.Smoke);
    }

    [Fact]
    public void OverpressurePredictsPipeBurst()
    {
        var level = LevelState.Create("Pressure", 6, 6)
                              .SetCell(new(1, 2), new() {
                                  Pipe = EPipeMedium.Pressure,
                                  Pressure = 10,
                                  Integrity = 6
                              });

        var forecasts = m_Engine.Forecast(level);

        Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.PipeBurst && forecast.Turns == 2);
    }

    [Fact]
    public void ForecastCapsFutureSimulationWhenNoTerminalConditionOccurs()
    {
        var engine = new SimulationEngine([], [], [new StepCountingHazard()]);
        var level = LevelState.Create("Stable", 6, 6);

        var forecasts = engine.Forecast(level);

        Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Count);
        Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Max(forecast => forecast.Turns));
    }

    [Fact]
    public void ForecastUsesCurrentBalancingProfile()
    {
        var previous = Balancing.Current;
        try
        {
            Balancing.Current = new TestBalancing();
            var engine = new SimulationEngine([], [], [new StepCountingHazard()]);
            var level = LevelState.Create("Stable", 6, 6);

            var forecasts = engine.Forecast(level);

            Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Count);
        }
        finally
        {
            Balancing.Current = previous;
        }
    }

    [Fact]
    public void ForecastPredictsMeltdownFromFutureSimulation()
    {
        var level = LevelState.Create("Meltdown", 6, 6)
                              .SetCell(new(2, 2), new() {
                                  Prop = ECellProp.Reactor,
                                  Hazards = new() { Heat = Balancing.Current.MeltdownCoreHeatThreshold - Balancing.Current.ReactorHeatIncrease }
                              });

        var forecasts = m_Engine.Forecast(level);

        Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Meltdown && forecast.Turns == 1);
    }

    [Fact]
    public void ForecastReportsAlreadyLostLevelAtCurrentTurn()
    {
        var level = LevelState.Create("Lost", 6, 6) with {
            Global = new() {
                CoreHeat = Balancing.Current.MeltdownCoreHeatThreshold,
                Lost = true,
                Status = "CORE MELTDOWN"
            }
        };

        var forecasts = m_Engine.Forecast(level);

        Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Meltdown && forecast.Turns == 0);
    }

    [Fact]
    public void ForecastPredictsStabilityCollapseFromFutureSimulation()
    {
        var level = LevelState.Create("Collapse", 6, 6)
                              .SetCell(new(2, 2), new() {
                                  Prop = ECellProp.Generator,
                                  Hazards = new() { Stability = Balancing.Current.CriticalCellStabilityThreshold }
                              }) with {
            Global = new() { FacilityStability = Balancing.Current.FireStabilityDamage }
        };

        var forecasts = m_Engine.Forecast(level);

        Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.StabilityCollapse && forecast.Turns == 1);
    }

    [Fact]
    public void StableReactorWithPowerAndCoolingCanActivate()
    {
        var level = LevelState.Create("Ready", 8, 6)
                              .SetCell(new(2, 2), new() {
                                  Prop = ECellProp.Reactor,
                                  Hazards = new() { Heat = 3 }
                              })
                              .SetCell(new(3, 2), new() {
                                  Prop = ECellProp.Generator,
                                  Powered = true
                              })
                              .SetCell(new(4, 2), new() {
                                  Prop = ECellProp.CoolingPump,
                                  Powered = true
                              });

        var next = m_Engine.AdvanceTurn(level);
        var activated = m_Engine.ActivateReactor(next);

        Assert.Equal("REACTOR ONLINE", activated.Global.Status);
        Assert.True(activated.Global.ReactorActivated);
    }

    [Fact]
    public void LevelSerializationRoundTripsEditableState()
    {
        var level = LevelState.Create("Round trip", 5, 5);
        level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Reactor);
        level = LevelEditor.Apply(level, new(1, 2), EEditorTool.CoolantPipe);
        level = LevelEditor.Apply(level, new(1, 2), EEditorTool.Leak);

        var json = LevelSerializer.Serialize(level);
        var loaded = LevelSerializer.Deserialize(json);

        Assert.Contains("\"Version\": 1", json);
        Assert.Equal(level.Name, loaded.Name);
        Assert.Equal(ECellProp.Reactor, loaded.GetCell(new(2, 2)).Prop);
        Assert.Equal(EPipeMedium.Coolant, loaded.GetCell(new(1, 2)).Pipe);
        Assert.Equal(1, loaded.GetCell(new(1, 2)).LeakRate);
    }

    [Fact]
    public void LevelSerializationRejectsUnsupportedVersion()
    {
        var json = """
                   {
                     "Version": 999,
                     "Level": {}
                   }
                   """;

        var exception = Assert.Throws<InvalidOperationException>(() => LevelSerializer.Deserialize(json));

        Assert.Contains("Unsupported level file version 999", exception.Message);
    }

    [Fact]
    public void WallToolClearsCellPropsPipesAndHazards()
    {
        var level = LevelState.Create("Wall", 5, 5);
        level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Generator);
        level = LevelEditor.Apply(level, new(2, 2), EEditorTool.CoolantPipe);
        level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Fire);

        var edited = LevelEditor.Apply(level, new(2, 2), EEditorTool.Wall);
        var cell = edited.GetCell(new(2, 2));

        Assert.Equal(ECellTerrain.Wall, cell.Terrain);
        Assert.Equal(ECellProp.None, cell.Prop);
        Assert.Equal(EPipeMedium.None, cell.Pipe);
        Assert.False(cell.Powered);
        Assert.False(cell.Hazards.Fire);
    }

    [Fact]
    public void PropToolsKeepFloorTerrain()
    {
        var level = LevelState.Create("Prop", 5, 5);
        level = LevelEditor.Apply(level, new(1, 1), EEditorTool.Wall);

        var edited = LevelEditor.Apply(level, new(1, 1), EEditorTool.Reactor);
        var cell = edited.GetCell(new(1, 1));

        Assert.Equal(ECellTerrain.Floor, cell.Terrain);
        Assert.Equal(ECellProp.Reactor, cell.Prop);
    }

    private readonly SimulationEngine m_Engine = new();

    private sealed class StepCountingHazard : Hazard
    {
        public override IEnumerable<Forecast> Predict(LevelState level, int turns)
        {
            yield return new(EFailureKind.PipeBurst, new(turns, 0), turns, $"STEP {turns}");
        }
    }

    private sealed class TestBalancing : NormalBalancing
    {
        public override int MaxForecastStepCount => 2;
    }

    private sealed class TestCellEffect : ISimulationEffect
    {
        public CellState Apply(CellState cell)
        {
            return cell with { Hazards = cell.Hazards with { Heat = 5 } };
        }
    }

    private sealed class TestAreaEffect : IAreaSimulationEffect
    {
        public CellState[] Apply(LevelState level, CellState[] cells)
        {
            var next = cells.ToArray();
            var position = new GridPosition(2, 2);
            var cell = next[level.Index(position)];
            next[level.Index(position)] = cell with { Hazards = cell.Hazards with { Smoke = 7 } };
            return next;
        }
    }
}