package controller_test

import (
	"maps"
	"slices"
	"testing"

	"galaxy/calc"
	"galaxy/game/internal/controller"
	"galaxy/game/internal/model/game"
	"galaxy/model/report"

	"github.com/stretchr/testify/assert"
)

var (
	attacker = game.ShipType{
		Name:     "Attacker",
		Drive:    8,
		Armament: 1,
		Weapons:  8,
		Shields:  8,
		Cargo:    0,
	}
	defender = game.ShipType{
		Name:     "Defender",
		Drive:    1,
		Armament: 1,
		Weapons:  1,
		Shields:  1,
		Cargo:    0,
	}
	ship = game.ShipType{
		Name:     "Ship",
		Drive:    10,
		Armament: 1,
		Weapons:  10,
		Shields:  10,
		Cargo:    0,
	}
)

func TestDestructionProbability(t *testing.T) {
	probability := calc.DestructionProbability(ship.Weapons.F(), 1, ship.Shields.F(), 1, ship.EmptyMass())
	assert.Equal(t, .5, probability)

	undefeatedShip := ship
	undefeatedShip.Shields = 55
	probability = calc.DestructionProbability(ship.Weapons.F(), 1, undefeatedShip.Shields.F(), 1, undefeatedShip.EmptyMass())
	assert.LessOrEqual(t, probability, 0.)

	disruptiveShip := ship
	disruptiveShip.Weapons = 40
	probability = calc.DestructionProbability(disruptiveShip.Weapons.F(), 1, ship.Shields.F(), 1, ship.EmptyMass())
	assert.GreaterOrEqual(t, probability, 1.)
}

func TestEffectiveDefence(t *testing.T) {
	assert.Equal(t, 10., calc.EffectiveDefence(ship.Shields.F(), 1, ship.EmptyMass()))

	attackerEffectiveDefence := calc.EffectiveDefence(attacker.Shields.F(), 1, attacker.EmptyMass())
	defenderEffectiveDefence := calc.EffectiveDefence(defender.Shields.F(), 1, defender.EmptyMass())

	// attacker's effective shields must be 'just' 4 times greater than defender's
	assert.InDelta(t, defenderEffectiveDefence*4, attackerEffectiveDefence, 0)
}

func TestCollectPlanetGroups(t *testing.T) {
	c, _ := newCache()

	assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 10))  // 1 #0
	assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 3)) // 2 #1
	assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3))   // 3 #2
	c.ShipGroup(2).StateInSpace = &InSpace                                             // 3 #2 -> In_Space
	assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1)) // 4 #3
	c.ShipGroup(3).Destination = R1_Planet_1_num                                       // 4 #3 -> Planet_1
	assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 15))  // 5 #4
	c.ShipGroup(4).Destination = R0_Planet_0_num                                       // 5 #4 -> Planet_0

	planetGroups := controller.CollectPlanetGroups(c)

	for pl := range planetGroups {
		switch pl {
		case R0_Planet_0_num:
			assert.Equal(t, 3, len(planetGroups[pl]))
			assert.Contains(t, planetGroups[pl], 0)
			assert.Contains(t, planetGroups[pl], 1)
			assert.Contains(t, planetGroups[pl], 4)
		default:
			assert.Fail(t, "planet #%d should not contain groups for battle", pl)
		}
	}
}

func TestFilterBattleOpponents(t *testing.T) {
	c, _ := newCache()

	assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1))   // 0
	assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1)) // 1
	assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 15))  // 2
	undefeatedShip := ship
	undefeatedShip.Shields = 100
	assert.NoError(t, c.ShipClassCreate(Race_1_idx, undefeatedShip.Name, undefeatedShip.Drive.F(), int(undefeatedShip.Armament), undefeatedShip.Weapons.F(), undefeatedShip.Shields.F(), undefeatedShip.Cargo.F()))
	assert.NoError(t, c.CreateShips(Race_1_idx, undefeatedShip.Name, R1_Planet_1_num, 1)) // 3

	cacheProbability := make(map[int]map[int]float64)

	assert.False(t, controller.FilterBattleOpponents(c, 0, 2, cacheProbability))
	assert.Contains(t, cacheProbability, 0)
	assert.Contains(t, cacheProbability[0], 2)
	assert.InDelta(t, 0.396222, cacheProbability[0][2], 0.0000001)
	assert.False(t, controller.FilterBattleOpponents(c, 2, 0, cacheProbability))
	assert.Contains(t, cacheProbability, 2)
	assert.Contains(t, cacheProbability[2], 0)
	assert.InDelta(t, 0.495, cacheProbability[2][0], 0.0001)

	// Test: same owner
	assert.True(t, controller.FilterBattleOpponents(c, 0, 0, cacheProbability))
	assert.True(t, controller.FilterBattleOpponents(c, 0, 1, cacheProbability))
	assert.True(t, controller.FilterBattleOpponents(c, 1, 0, cacheProbability))

	// Test: reace reations
	assert.NoError(t, c.UpdateRelation(Race_0_idx, Race_1_idx, game.RelationPeace))
	assert.True(t, controller.FilterBattleOpponents(c, 0, 2, cacheProbability))
	assert.True(t, controller.FilterBattleOpponents(c, 2, 0, cacheProbability))
	assert.NoError(t, c.UpdateRelation(Race_0_idx, Race_1_idx, game.RelationWar))

	assert.LessOrEqual(t, calc.DestructionProbability(Cruiser.Weapons.F(), 1, undefeatedShip.Shields.F(), 1, undefeatedShip.EmptyMass()), 0.)
	assert.True(t, controller.FilterBattleOpponents(c, 1, 3, cacheProbability))
	assert.NotContains(t, cacheProbability[1], 3)
}

func TestProduceBattles(t *testing.T) {
	c, g := newCache()

	race_C_name, race_D_name := "Race_C", "Race_D"
	race_C_idx, _ := c.AddRace(race_C_name)
	race_D_idx, _ := c.AddRace(race_D_name)

	assert.NoError(t, g.RaceRelation(Race_0.Name, Race_1.Name, game.RelationWar.String()))
	assert.NoError(t, g.RaceRelation(Race_1.Name, Race_0.Name, game.RelationWar.String()))

	assert.NoError(t, g.RaceRelation(race_C_name, race_D_name, game.RelationWar.String()))
	assert.NoError(t, g.RaceRelation(race_D_name, race_C_name, game.RelationWar.String()))

	assert.Equal(t, game.RelationPeace, c.Relation(Race_0_idx, race_C_idx))
	assert.Equal(t, game.RelationPeace, c.Relation(Race_1_idx, race_C_idx))
	assert.Equal(t, game.RelationPeace, c.Relation(Race_0_idx, race_D_idx))
	assert.Equal(t, game.RelationPeace, c.Relation(Race_1_idx, race_D_idx))

	// Race_0
	assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 10))

	// Race_1
	c.CreateShipsUnsafe_T(Race_1_idx, c.MustShipClass(Race_1_idx, Race_1_Gunship).ID, R0_Planet_0_num, 11)

	// Race_C
	assert.NoError(t, c.ShipClassCreate(race_C_idx, Cruiser.Name, Cruiser.Drive.F(), int(Cruiser.Armament), Cruiser.Weapons.F(), Cruiser.Shields.F(), Cruiser.Cargo.F()))
	c.CreateShipsUnsafe_T(race_C_idx, c.MustShipClass(race_C_idx, Cruiser.Name).ID, R0_Planet_0_num, 12)

	// Race_D
	assert.NoError(t, c.ShipClassCreate(race_D_idx, Cruiser.Name, Cruiser.Drive.F(), int(Cruiser.Armament), Cruiser.Weapons.F(), Cruiser.Shields.F(), Cruiser.Cargo.F()))
	c.CreateShipsUnsafe_T(race_D_idx, c.MustShipClass(race_D_idx, Cruiser.Name).ID, R0_Planet_0_num, 13)

	battle := controller.ProduceBattles(c)

	assert.Len(t, battle, 1)
	b := battle[0]
	assert.Equal(t, R0_Planet_0_num, b.Planet)
	assert.Len(t, b.ObserverGroups, 4)
	assert.Len(t, b.InitialNumbers, 4)
	assert.ElementsMatch(t, slices.Collect(maps.Keys(b.ObserverGroups)), slices.Collect(maps.Keys(b.InitialNumbers)))
	assert.Equal(t, 10, int(b.InitialNumbers[0]))
	assert.Equal(t, 11, int(b.InitialNumbers[1]))
	assert.Equal(t, 12, int(b.InitialNumbers[2]))
	assert.Equal(t, 13, int(b.InitialNumbers[3]))
	if c.ShipGroup(0).Number == 0 {
		assert.Greater(t, c.ShipGroup(1).Number, uint(0))
	} else {
		assert.Zero(t, c.ShipGroup(1).Number)
	}
	if c.ShipGroup(2).Number == 0 {
		assert.Greater(t, c.ShipGroup(3).Number, uint(0))
	} else {
		assert.Zero(t, c.ShipGroup(3).Number)
	}
}

// TestTransformBattleAggregatesSameShipClass guards against the
// engine-side variant of the duplicate-class bug. Several ShipGroups
// of the same ShipClass.ID can take part in the same battle (arrivals
// from different planets, tech splits, etc.); they must collapse into
// a single BattleReportGroup with summed Number and NumberLeft. The
// pre-fix engine cached the first group's index and silently dropped
// every subsequent group's initial / survivor counts, which manifested
// downstream as more Destroyed shots in the protocol than the
// recorded initial roster could account for.
func TestTransformBattleAggregatesSameShipClass(t *testing.T) {
	c, g := newCache()

	assert.NoError(t, g.RaceRelation(Race_0.Name, Race_1.Name, game.RelationWar.String()))
	assert.NoError(t, g.RaceRelation(Race_1.Name, Race_0.Name, game.RelationWar.String()))

	// Two Race_0 groups of the SAME ship class (Race_0_Gunship) plus
	// one Race_1 group of Race_1_Gunship — all parked on Planet_0
	// (owned by Race_0; the Race_1 group lands there via the Unsafe
	// helper that bypasses the ownership check). Group indices land
	// at 0, 1, 2 in creation order.
	assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 10))
	assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 10))
	c.CreateShipsUnsafe_T(Race_1_idx, c.MustShipClass(Race_1_idx, Race_1_Gunship).ID, R0_Planet_0_num, 5)

	// Simulate post-battle survivor counts: Group 0 ended the battle
	// with 8 ships, Group 1 with 6. The aggregated BattleReportGroup
	// must report NumberLeft = 8 + 6 = 14 (not just the last cached
	// group's 6 — that's the regression).
	c.ShipGroup(0).Number = 8
	c.ShipGroup(1).Number = 6

	b := &controller.Battle{
		Planet:         R0_Planet_0_num,
		ObserverGroups: map[int]bool{0: true, 1: true, 2: true},
		InitialNumbers: map[int]uint{0: 10, 1: 10, 2: 5},
		// Protocol must reference every in-battle group at least once
		// (otherwise TransformBattle won't register it through the
		// `ship()` path). Two shots from Race_1 against each Race_0
		// group hits both groupIds.
		Protocol: []controller.BattleAction{
			{Attacker: 2, Defender: 0, Destroyed: true},
			{Attacker: 2, Defender: 1, Destroyed: true},
		},
	}

	r := controller.TransformBattle(c, b)

	// Two BattleReportGroup entries total: one merged Race_0_Gunship
	// (groups 0 + 1) and one Race_1_Gunship. NOT three.
	if got, want := len(r.Ships), 2; got != want {
		t.Fatalf("len(r.Ships) = %d, want %d (duplicate ShipClass.ID must merge)", got, want)
	}

	var gunship0, gunship1 *report.BattleReportGroup
	for i := range r.Ships {
		grp := r.Ships[i]
		switch grp.Race {
		case Race_0.Name:
			gunship0 = &grp
		case Race_1.Name:
			gunship1 = &grp
		}
	}
	if gunship0 == nil || gunship1 == nil {
		t.Fatalf("missing race entry: race0=%v race1=%v", gunship0, gunship1)
	}

	if gunship0.ClassName != Race_0_Gunship {
		t.Errorf("race0.ClassName = %q, want %q", gunship0.ClassName, Race_0_Gunship)
	}
	if gunship0.Number != 20 {
		t.Errorf("race0.Number = %d, want 20 (10+10)", gunship0.Number)
	}
	if gunship0.NumberLeft != 14 {
		t.Errorf("race0.NumberLeft = %d, want 14 (8+6)", gunship0.NumberLeft)
	}
	if !gunship0.InBattle {
		t.Errorf("race0.InBattle = false, want true (both source groups were in-battle)")
	}

	if gunship1.Number != 5 || gunship1.NumberLeft != 5 {
		t.Errorf("race1 = (Number=%d, NumberLeft=%d), want (5, 5)",
			gunship1.Number, gunship1.NumberLeft)
	}
}
