namespace splat;

using System.Threading.Tasks;
using Chickensoft.GoDotTest;
using Chickensoft.GodotTestDriver;
using Godot;
using Shouldly;
using System.Linq;

public class GameTest : TestClass
{
    private Game _game = default!;
    private Fixture _fixture = default!;
    private SubViewport _viewport = default!;

    public GameTest(Node testScene) : base(testScene) { }

    [SetupAll]
    public async Task Setup()
    {
        _fixture = new Fixture(TestScene.GetTree());
        _viewport = new SubViewport
        {
            Size = new Vector2I(720, 720),
            OwnWorld3D = true,
            RenderTargetUpdateMode = SubViewport.UpdateMode.Always
        };
        TestScene.AddChild(_viewport);
        _game = await _fixture.LoadAndAddScene<Game>(
            autoFree: true,
            autoRemoveFromRoot: false
        );
        _game.Reparent(_viewport);
        await TestScene.ToSignal(TestScene.GetTree(), SceneTree.SignalName.ProcessFrame);
    }

    [CleanupAll]
    public void Cleanup()
    {
        _fixture.Cleanup();
        _viewport.QueueFree();
    }

    [Test]
    public void SceneWiresUpCoreSystems()
    {
        _game.ShouldNotBeNull();
        _game.Manager.ShouldNotBeNull();
        _game.Buildings.ShouldNotBeNull();
        _game.Input.ShouldNotBeNull();
        _game.Ground.ShouldNotBeNull();
        _game.Traffic.ShouldNotBeNull();
        _game.BuildingScene.ShouldNotBeNull();
        _game.RoadScene.ShouldNotBeNull();
        _game.Hud.ShouldNotBeNull();
        _game.BuildMenu.ShouldNotBeNull();
        _game.GetNode<Node3D>("World/Buildings").ShouldNotBeNull();
        _game.GetNode<Node3D>("World/Roads").ShouldNotBeNull();
        _game.GetNode<Node3D>("World/Vehicles").ShouldNotBeNull();
        _game.Buildings.Buildings.Count.ShouldBeGreaterThanOrEqualTo(1);
    }

    [Test]
    public void BuildingDataComputesMassFromVolumeAndDensity()
    {
        var data = new BuildingData
        {
            Material = MaterialType.Stone,
            Volume = 10f
        };
        data.Init();

        // Stone density ~2400 kg/m³.
        data.Mass.ShouldBe(24000f);
    }

    [Test]
    public void BuildingManagerTracksRegistrations()
    {
        var before = _game.Buildings.Buildings.Count;

        var data = new BuildingData { Material = MaterialType.Timber, Volume = 50f };
        data.Init();
        var building = new Building { Data = data };
        _game.AddChild(building);
        _game.Buildings.Register(building);

        _game.Buildings.Buildings.Count.ShouldBe(before + 1);

        _game.Buildings.Unregister(building);
        building.QueueFree();
    }

    [Test]
    public void BuildingSceneOwnsPhysicalAndVisualStructure()
    {
        var first = _game.BuildingScene.Instantiate<Building>();
        var second = _game.BuildingScene.Instantiate<Building>();

        first.Data.ShouldNotBeNull();
        first.GetNodeOrNull<CollisionShape3D>("CollisionShape3D").ShouldNotBeNull();
        first.GetNodeOrNull<MeshInstance3D>("MeshInstance3D").ShouldNotBeNull();
        first.GetNodeOrNull<MeshInstance3D>("Roof").ShouldNotBeNull();
        ReferenceEquals(first.Data, second.Data).ShouldBeFalse();

        first.Free();
        second.Free();
    }

    [Test]
    public void BuildMenuSelectsToolAndShowsReticle()
    {
        _game.Input.SetTool(InputManager.Tool.Select);
        _game.BuildMenu.Open();
        _game.BuildMenu.Visible.ShouldBeTrue();

        var cottageButton = _game.BuildMenu.GetNode<Button>(
            "Panel/Margin/Content/Tools/CottageButton"
        );
        cottageButton.EmitSignal(Button.SignalName.Pressed);

        _game.Input.CurrentTool.ShouldBe(InputManager.Tool.PlaceBuilding);
        _game.BuildMenu.Visible.ShouldBeFalse();
        _game.Hud.GetNode<Control>("PlacementReticle").Visible.ShouldBeTrue();
    }

    [Test]
    public void ControllerOwnsPrimaryBuildActions()
    {
        InputMap.HasAction("placement_confirm").ShouldBeTrue();
        InputMap.HasAction("placement_cancel").ShouldBeTrue();
        InputMap.HasAction("game_cycle_speed").ShouldBeTrue();

        InputMap.ActionGetEvents("placement_confirm")
            .OfType<InputEventJoypadButton>()
            .Any(input => input.ButtonIndex == JoyButton.A)
            .ShouldBeTrue();
        InputMap.ActionGetEvents("placement_cancel")
            .OfType<InputEventJoypadButton>()
            .Any(input => input.ButtonIndex == JoyButton.B)
            .ShouldBeTrue();
        InputMap.ActionGetEvents("ui_build_menu")
            .OfType<InputEventJoypadButton>()
            .Any(input => input.ButtonIndex == JoyButton.Y)
            .ShouldBeTrue();
    }

    [Test]
    public void MenuAndCancelActionsDriveTheGameState()
    {
        _game.Input.SetTool(InputManager.Tool.Select);
        _game.BuildMenu.Close();

        _game.Input._UnhandledInput(new InputEventAction
        {
            Action = "ui_build_menu",
            Pressed = true
        });
        _game.BuildMenu.Visible.ShouldBeTrue();

        _game.Input._UnhandledInput(new InputEventAction
        {
            Action = "placement_cancel",
            Pressed = true
        });
        _game.BuildMenu.Visible.ShouldBeFalse();

        _game.Input.SetTool(InputManager.Tool.PlaceRoad);
        _game.Input._UnhandledInput(new InputEventAction
        {
            Action = "placement_cancel",
            Pressed = true
        });
        _game.Input.CurrentTool.ShouldBe(InputManager.Tool.Select);
    }

    [Test]
    public async Task ControllerConfirmPlacesAtTheScreenReticle()
    {
        await TestScene.ToSignal(TestScene.GetTree(), SceneTree.SignalName.PhysicsFrame);
        var before = _game.Buildings.Buildings.Count;
        _game.Input.SetTool(InputManager.Tool.PlaceBuilding);

        _game._UnhandledInput(new InputEventAction
        {
            Action = "placement_confirm",
            Pressed = true
        });

        _game.Buildings.Buildings.Count.ShouldBe(before + 1);
        var placed = _game.Buildings.Buildings[^1];
        placed.Position.X.ShouldBe(0f, tolerance: 0.01f);
        placed.Position.Z.ShouldBe(0f, tolerance: 0.01f);
    }

    [Test]
    public void SpeedActionCyclesSimulationSpeed()
    {
        var before = _game.Manager.CurrentSpeedIndex;
        _game.Manager._UnhandledInput(new InputEventAction
        {
            Action = "game_cycle_speed",
            Pressed = true
        });

        _game.Manager.CurrentSpeedIndex.ShouldBe((before + 1) % GameManager.SpeedSteps.Length);
    }
}
