namespace ReactorMaintenance.Simulation.Tests;

public sealed class SimulationEngineTests
{
    [Fact]
    public void NetworkPropagationSuppliesBoundConsumersAndReadiesReactor()
    {
        var level = BuildReadyLevel();

        var next = m_Engine.AdvanceTurn(level);

        Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 2)).ServiceState);
        Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 3)).ServiceState);
        Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 4)).ServiceState);
        Assert.Equal(ELevelState.Ready, next.Global.LevelState);
        Assert.Contains(next.Forecasts, forecast => forecast.Kind == EForecastKind.ReactorReady);
    }

    [Fact]
    public void ReactorActivatesOnlyAtReadyControl()
    {
        var level = m_Engine.AdvanceTurn(BuildReadyLevel()) with {
            Robot = new() { Position = new(5, 3) }
        };

        var activated = m_Engine.ActivateReactor(level);

        Assert.Equal(ELevelState.Won, activated.Global.LevelState);
        Assert.True(activated.Reactors[0].Activated);
    }

    [Fact]
    public void LeakingUndergroundCellInjectsMatchingSurfaceHazard()
    {
        var level = LevelState.Create("Leak", 6, 6);
        level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Leaking, Amount = 5, Intensity = 5 }) with {
            Leaks = [new() { Carrier = ECarrierType.Fuel, UndergroundPosition = new(2, 2), AccessPosition = new(2, 2) }]
        };
        level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });

        var next = m_Engine.AdvanceTurn(level);

        Assert.True(next.GetSurface(new(2, 2)).Fuel > 0);
    }

    [Fact]
    public void ElementRemedyClearsHazardAndBlocksImmediateReentry()
    {
        var level = LevelState.Create("Remedy", 6, 6);
        level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Leaking, Amount = 5, Intensity = 5 });
        level = level.SetSurface(new(2, 2), new() { Fuel = 5 }) with {
            Robot = new() { Position = new(2, 2), FuelNeutralizers = 1 },
            Leaks = [new() { Carrier = ECarrierType.Fuel, UndergroundPosition = new(2, 2), AccessPosition = new(2, 2) }]
        };

        var next = m_Engine.InteractLeak(level, ECarrierType.Fuel, true);

        Assert.Equal(0, next.GetSurface(new(2, 2)).Fuel);
        Assert.True(next.GetSurface(new(2, 2)).FuelBlockTurns > 0);
        Assert.Equal(0, next.Robot.FuelNeutralizers);
    }

    [Fact]
    public void ClosedDoorBlocksAdjacentHeatFlow()
    {
        var level = LevelState.Create("Door", 6, 6);
        level = level.SetSurface(new(2, 2), new() { Heat = 8 }) with {
            Doors = [new() { A = new(2, 2), B = new(3, 2), State = EDoorState.Closed }]
        };

        var next = m_Engine.AdvanceTurn(level);

        Assert.Equal(0, next.GetSurface(new(3, 2)).Heat);
    }

    [Fact]
    public void HeatShieldPreventsRobotHeatLoss()
    {
        var level = LevelState.Create("Heat shield", 6, 6);
        level = level.SetSurface(new(2, 2), new() { Heat = Balancing.Current.RobotHeatSafetyThreshold }) with {
            Robot = new() { Position = new(2, 2), HeatImmunitySteps = 1 }
        };

        var next = m_Engine.AdvanceTurn(level);

        Assert.NotEqual(ELevelState.Lost, next.Global.LevelState);
    }

    [Fact]
    public void JunctionRatioSplitsFlowAcrossInferredOutgoingBranches()
    {
        var level = BuildJunctionLevel(2);

        var next = m_Engine.AdvanceTurn(level);

        Assert.True(next.GetUnderground(new(2, 2), ECarrierType.Fuel).Amount > 0);
        Assert.Equal(next.GetUnderground(new(2, 2), ECarrierType.Fuel).Amount, next.GetUnderground(new(3, 3), ECarrierType.Fuel).Amount);
        Assert.True(next.GetUnderground(new(2, 2), ECarrierType.Fuel).Amount < next.GetUnderground(new(2, 3), ECarrierType.Fuel).Amount);
    }

    [Fact]
    public void JunctionZeroWeightBranchReceivesNoIntentionalOutflow()
    {
        var level = BuildJunctionLevel(0);

        var next = m_Engine.AdvanceTurn(level);

        Assert.Equal(0, next.GetUnderground(new(2, 2), ECarrierType.Fuel).Amount);
        Assert.True(next.GetUnderground(new(3, 3), ECarrierType.Fuel).Amount > 0);
    }

    [Fact]
    public void ValidatorRejectsAmbiguousJunctionSourceBranches()
    {
        var level = BuildJunctionLevel(2);
        level = level.SetProp(new(3, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });

        var report = new LevelValidator().Validate(level);

        Assert.False(report.IsValid);
        Assert.Contains(report.Errors, error => error.Message.Contains("Ambiguous junction flow", StringComparison.Ordinal));
    }

    [Fact]
    public void ValidatorRejectsJunctionWithoutTwoOrThreeOutflows()
    {
        var level = BuildJunctionLevel(2);
        level = level.SetUnderground(new(3, 3), ECarrierType.Fuel, new());

        var report = new LevelValidator().Validate(level);

        Assert.False(report.IsValid);
        Assert.Contains(report.Errors, error => error.Message.Contains("one incoming branch and two or three outgoing branches", StringComparison.Ordinal));
    }

    [Fact]
    public void NonJunctionCellsAcceptBestFlowFromMultipleSourcePaths()
    {
        var level = LevelState.Create("Best path", 7, 7);
        level = AddHorizontalUnderground(level, ECarrierType.Fuel, 1, 5, 3);
        level = level.SetProp(new(1, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
        level = level.SetProp(new(5, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
        level = level.SetProp(new(3, 3), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Fuel });

        var report = new LevelValidator().Validate(level);
        var next = m_Engine.AdvanceTurn(level);

        Assert.True(report.IsValid);
        Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 3)).ServiceState);
    }

    [Fact]
    public void RobotLosesOnUnsafeElementHazard()
    {
        var level = LevelState.Create("Unsafe", 6, 6);
        level = level.SetSurface(new(2, 2), new() { Electricity = Balancing.Current.MaxValue }) with {
            Robot = new() { Position = new(2, 2) }
        };

        var next = m_Engine.AdvanceTurn(level);

        Assert.Equal(ELevelState.Lost, next.Global.LevelState);
    }

    [Fact]
    public void RuleEventCanCreateTerminalLossForecast()
    {
        var level = LevelState.Create("Rule", 6, 6) with {
            RuleEvents = [
                new() {
                    Phase = ERuleEventPhase.EndOfTurn,
                    ForecastText = "containment failure",
                    Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 0 }],
                    Effects = [new() { Kind = ERuleEffectKind.MarkTerminalLoss, Message = "CONTAINMENT FAILURE" }]
                }
            ]
        };

        var forecasts = m_Engine.Forecast(level);

        Assert.Contains(forecasts, forecast => forecast.Kind == EForecastKind.RuleEvent && forecast.Message == "containment failure");
        Assert.Contains(forecasts, forecast => forecast.Kind == EForecastKind.TerminalLoss);
    }

    [Fact]
    public void RuleEventCanTriggerFromNetworkBand()
    {
        var level = LevelState.Create("Network rule", 6, 6);
        level = AddLine(level, ECarrierType.Fuel, new(2, 2), new(3, 2));
        level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel }) with {
            RuleEvents = [
                new() {
                    Phase = ERuleEventPhase.EndOfTurn,
                    Predicates = [new() { Kind = ERulePredicateKind.NetworkBandAt, Position = new(3, 2), Carrier = ECarrierType.Fuel, NetworkValue = ENetworkValueKind.Amount, Band = EBand.Critical }],
                    Effects = [new() { Kind = ERuleEffectKind.EmitWarning, Message = "fuel pressure high" }]
                }
            ]
        };

        var next = m_Engine.AdvanceTurn(level);

        Assert.Contains("fuel pressure high", next.Global.Warnings);
    }

    [Fact]
    public void RuleEventCanTriggerFromReactorReadiness()
    {
        var level = BuildReadyLevel() with {
            RuleEvents = [
                new() {
                    Phase = ERuleEventPhase.EndOfTurn,
                    Predicates = [new() { Kind = ERulePredicateKind.ReactorReadyIs, ReactorId = 1, BoolValue = true }],
                    Effects = [new() { Kind = ERuleEffectKind.EmitWarning, Message = "reactor ready rule" }]
                }
            ]
        };

        var next = m_Engine.AdvanceTurn(level);

        Assert.Contains("reactor ready rule", next.Global.Warnings);
    }

    [Fact]
    public void RuleEventCanTriggerFromRobotInventory()
    {
        var level = LevelState.Create("Inventory rule", 6, 6) with {
            Robot = new() { Position = new(1, 1), FuelNeutralizers = 1 },
            RuleEvents = [
                new() {
                    Phase = ERuleEventPhase.StartOfSimulation,
                    Predicates = [new() { Kind = ERulePredicateKind.RobotInventoryAtLeast, Remedy = ERemedyType.FuelNeutralizer, InventoryCount = 1 }],
                    Effects = [new() { Kind = ERuleEffectKind.EmitWarning, Message = "fuel kit detected" }]
                }
            ]
        };

        var next = m_Engine.AdvanceTurn(level);

        Assert.Contains("fuel kit detected", next.Global.Warnings);
    }

    [Fact]
    public void RuleEventCanRemoveHazardsHeatAndInventory()
    {
        var level = LevelState.Create("Remove rule", 6, 6);
        level = level.SetSurface(new(2, 2), new() { Fuel = 5, Heat = 5 }) with {
            Robot = new() { Position = new(1, 1), FuelNeutralizers = 2 },
            Doors = [
                new() { A = new(2, 2), B = new(1, 2), State = EDoorState.Closed },
                new() { A = new(2, 2), B = new(3, 2), State = EDoorState.Closed },
                new() { A = new(2, 2), B = new(2, 1), State = EDoorState.Closed },
                new() { A = new(2, 2), B = new(2, 3), State = EDoorState.Closed }
            ],
            RuleEvents = [
                new() {
                    Phase = ERuleEventPhase.StartOfSimulation,
                    Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 0 }],
                    Effects = [
                        new() { Kind = ERuleEffectKind.RemoveSurfaceHazard, Position = new(2, 2), Carrier = ECarrierType.Fuel, Amount = 2 },
                        new() { Kind = ERuleEffectKind.RemoveHeat, Position = new(2, 2), Amount = 3 },
                        new() { Kind = ERuleEffectKind.RemoveInventory, Remedy = ERemedyType.FuelNeutralizer, Amount = 1 }
                    ]
                }
            ]
        };

        var next = m_Engine.AdvanceTurn(level);

        Assert.Equal(3, next.GetSurface(new(2, 2)).Fuel);
        Assert.Equal(2, next.GetSurface(new(2, 2)).Heat);
        Assert.Equal(1, next.Robot.FuelNeutralizers);
    }

    [Fact]
    public void RuleEventStartLeakUsesAuthoredElectricityAccessFace()
    {
        var level = LevelState.Create("Electricity leak rule", 6, 6);
        level = level.SetTerrain(new(2, 2), ECellTerrain.Wall);
        level = level.SetUnderground(new(2, 2), ECarrierType.Electricity, new() { State = EUndergroundState.Intact }) with {
            RuleEvents = [
                new() {
                    Phase = ERuleEventPhase.StartOfSimulation,
                    Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 0 }],
                    Effects = [new() { Kind = ERuleEffectKind.StartLeak, Position = new(2, 2), AccessPosition = new(2, 3), Carrier = ECarrierType.Electricity }]
                }
            ]
        };

        var next = m_Engine.AdvanceTurn(level);

        Assert.Single(next.Leaks);
        Assert.Equal(new(2, 3), next.Leaks[0].AccessPosition);
        Assert.True(next.GetSurface(new(2, 3)).Electricity > 0);
    }

    [Fact]
    public void ValidatorRejectsInvalidRuleTargets()
    {
        var level = LevelState.Create("Invalid rules", 6, 6);
        level = level.SetTerrain(new(2, 2), ECellTerrain.Wall) with {
            RuleEvents = [
                new() {
                    Predicates = [new() { Kind = ERulePredicateKind.PropStateAt, Position = new(1, 1) }],
                    Effects = [
                        new() { Kind = ERuleEffectKind.AddSurfaceHazard, Position = new(2, 2), Carrier = ECarrierType.Fuel, Amount = 1 },
                        new() { Kind = ERuleEffectKind.RepairNetworkCell, Position = new(3, 3), Carrier = ECarrierType.Coolant }
                    ]
                }
            ]
        };

        var report = new LevelValidator().Validate(level);

        Assert.False(report.IsValid);
        Assert.Contains(report.Errors, error => error.Message.Contains("Rule prop predicate", StringComparison.Ordinal));
        Assert.Contains(report.Errors, error => error.Message.Contains("Rule surface effect", StringComparison.Ordinal));
        Assert.Contains(report.Errors, error => error.Message.Contains("Rule network effect", StringComparison.Ordinal));
    }

    [Fact]
    public void ValidatorRejectsWallHazardsAndInvalidReactorBinding()
    {
        var level = LevelState.Create("Invalid", 6, 6);
        level = level.SetTerrain(new(2, 2), ECellTerrain.Wall);
        level = level with {
            Surface = level.Surface.ToArray(),
            Reactors = [new() { ControlPosition = new(3, 3), FuelConsumerPosition = new(1, 1), CoolantConsumerPosition = new(1, 1), ElectricityConsumerPosition = new(1, 1) }]
        };
        level.Surface[level.Index(new(2, 2))] = new() { Heat = 1 };

        var report = new LevelValidator().Validate(level);

        Assert.False(report.IsValid);
        Assert.Contains(report.Errors, error => error.Message.Contains("Wall cell", StringComparison.Ordinal));
        Assert.Contains(report.Errors, error => error.Message.Contains("Reactor binding", StringComparison.Ordinal));
    }

    [Fact]
    public void LevelSerializationRoundTripsCurrentSchemaOnly()
    {
        var level = BuildReadyLevel();

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

        Assert.Contains("\"Version\": 2", json);
        Assert.Equal(level.Name, loaded.Name);
        Assert.Equal(EPropType.Flow, loaded.GetProp(new(2, 2)).Type);
    }

    [Fact]
    public void LevelSerializationRoundTripsRuleEventsDoorsAndElectricityLeakFaces()
    {
        var level = BuildReadyLevel();
        level = level.SetTerrain(new(6, 4), ECellTerrain.Wall);
        level = level.SetUnderground(new(6, 4), ECarrierType.Electricity, new() { State = EUndergroundState.Leaking }) with {
            Doors = [new() { A = new(5, 3), B = new(5, 4), State = EDoorState.Closed }],
            Leaks = [new() { Carrier = ECarrierType.Electricity, UndergroundPosition = new(6, 4), AccessPosition = new(6, 3) }],
            RuleEvents = [
                new() {
                    Id = "authored",
                    Phase = ERuleEventPhase.EndOfTurn,
                    Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 2 }],
                    Effects = [new() { Kind = ERuleEffectKind.EmitWarning, Message = "serialized" }]
                }
            ]
        };

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

        Assert.Single(loaded.Doors);
        Assert.Single(loaded.Leaks);
        Assert.Single(loaded.RuleEvents);
        Assert.Equal(new(6, 3), loaded.Leaks[0].AccessPosition);
        Assert.Equal("authored", loaded.RuleEvents[0].Id);
    }

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

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

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

    private static LevelState BuildReadyLevel()
    {
        var level = LevelState.Create("Ready", 8, 7);
        level = AddLine(level, ECarrierType.Fuel, new(2, 2), new(3, 2));
        level = AddLine(level, ECarrierType.Coolant, new(2, 3), new(3, 3));
        level = AddLine(level, ECarrierType.Electricity, new(2, 4), new(3, 4));
        level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
        level = level.SetProp(new(2, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Coolant });
        level = level.SetProp(new(2, 4), new() { Type = EPropType.Flow, Carrier = ECarrierType.Electricity });
        level = level.SetProp(new(3, 2), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Fuel });
        level = level.SetProp(new(3, 3), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Coolant });
        level = level.SetProp(new(3, 4), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Electricity });
        level = level.SetProp(new(5, 3), new() { Type = EPropType.ReactorControl, ReactorId = 1 });
        return level with {
            Robot = new() { Position = new(5, 3) },
            Reactors = [
                new() {
                    ReactorId = 1,
                    ControlPosition = new(5, 3),
                    FuelConsumerPosition = new(3, 2),
                    CoolantConsumerPosition = new(3, 3),
                    ElectricityConsumerPosition = new(3, 4)
                }
            ]
        };
    }

    private static LevelState AddLine(LevelState level, ECarrierType carrier, GridPosition a, GridPosition b)
    {
        level = level.SetUnderground(a, carrier, new() { State = EUndergroundState.Intact });
        level = level.SetUnderground(b, carrier, new() { State = EUndergroundState.Intact });
        return level;
    }

    private static LevelState BuildJunctionLevel(int mode)
    {
        var level = LevelState.Create("Junction", 6, 6);
        level = level.SetUnderground(new(1, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
        level = level.SetUnderground(new(2, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
        level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
        level = level.SetUnderground(new(3, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
        level = level.SetProp(new(1, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
        return level.SetProp(new(2, 3), new() { Type = EPropType.Junction, Carrier = ECarrierType.Fuel, JunctionMode = mode });
    }

    private static LevelState AddHorizontalUnderground(LevelState level, ECarrierType carrier, int startX, int endX, int y)
    {
        for (var x = startX; x <= endX; x++)
            level = level.SetUnderground(new(x, y), carrier, new() { State = EUndergroundState.Intact });

        return level;
    }

    private readonly SimulationEngine m_Engine = new();
}