package transcoder

import (
	"bytes"
	"reflect"
	"strconv"
	"strings"
	"testing"

	model "galaxy/model/report"
	fbs "galaxy/schema/fbs/battle"

	flatbuffers "github.com/google/flatbuffers/go"
	"github.com/google/uuid"
)

func TestBattleReportToPayloadAndPayloadToBattleReportRoundTrip(t *testing.T) {
	t.Parallel()

	source := sampleBattleReport()

	payload, err := BattleReportToPayload(source)
	if err != nil {
		t.Fatalf("encode battle report payload: %v", err)
	}

	decoded, err := PayloadToBattleReport(payload)
	if err != nil {
		t.Fatalf("decode battle report payload: %v", err)
	}

	expected := battleReportWireClone(t, source)
	if !reflect.DeepEqual(expected, decoded) {
		t.Fatalf("round-trip mismatch\nexpected: %#v\ndecoded:  %#v", expected, decoded)
	}
}

func TestBattleReportToPayloadNilReport(t *testing.T) {
	t.Parallel()

	_, err := BattleReportToPayload(nil)
	if err == nil {
		t.Fatal("expected error for nil battle report")
	}
}

func TestPayloadToBattleReportEmptyData(t *testing.T) {
	t.Parallel()

	_, err := PayloadToBattleReport(nil)
	if err == nil {
		t.Fatal("expected error for empty payload")
	}
}

func TestPayloadToBattleReportGarbageDataDoesNotPanic(t *testing.T) {
	t.Parallel()

	_, err := PayloadToBattleReport([]byte{0x01, 0x02, 0x03})
	if err == nil {
		t.Fatal("expected error for malformed payload")
	}
}

func TestPayloadToBattleReportMissingID(t *testing.T) {
	t.Parallel()

	payload := buildBattleReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
		fbs.BattleReportStart(builder)
		fbs.BattleReportAddPlanet(builder, 7)
		return fbs.BattleReportEnd(builder)
	})

	_, err := PayloadToBattleReport(payload)
	if err == nil {
		t.Fatal("expected error for missing battle report id")
	}
	if !strings.Contains(err.Error(), "id is missing") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestPayloadToBattleReportMissingRaceValue(t *testing.T) {
	t.Parallel()

	payload := buildBattleReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
		fbs.RaceEntryStart(builder)
		fbs.RaceEntryAddKey(builder, 1)
		race := fbs.RaceEntryEnd(builder)

		fbs.BattleReportStartRacesVector(builder, 1)
		builder.PrependUOffsetT(race)
		races := builder.EndVector(1)

		fbs.BattleReportStart(builder)
		fbs.BattleReportAddId(builder, fbs.CreateUUID(builder, 1, 2))
		fbs.BattleReportAddRaces(builder, races)
		return fbs.BattleReportEnd(builder)
	})

	_, err := PayloadToBattleReport(payload)
	if err == nil {
		t.Fatal("expected error for missing race value")
	}
	if !strings.Contains(err.Error(), "race value is missing") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestPayloadToBattleReportMissingShipValue(t *testing.T) {
	t.Parallel()

	payload := buildBattleReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
		fbs.ShipEntryStart(builder)
		fbs.ShipEntryAddKey(builder, 1)
		ship := fbs.ShipEntryEnd(builder)

		fbs.BattleReportStartShipsVector(builder, 1)
		builder.PrependUOffsetT(ship)
		ships := builder.EndVector(1)

		fbs.BattleReportStart(builder)
		fbs.BattleReportAddId(builder, fbs.CreateUUID(builder, 1, 2))
		fbs.BattleReportAddShips(builder, ships)
		return fbs.BattleReportEnd(builder)
	})

	_, err := PayloadToBattleReport(payload)
	if err == nil {
		t.Fatal("expected error for missing ship value")
	}
	if !strings.Contains(err.Error(), "ship value is missing") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestPayloadToBattleReportOverflowInt(t *testing.T) {
	t.Parallel()

	if strconv.IntSize == 64 {
		t.Skip("int overflow from int64 is not possible on 64-bit runtime")
	}

	maxInt := int(^uint(0) >> 1)
	overflowValue := int64(maxInt) + 1
	payload := buildBattleReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
		fbs.BattleActionReportStart(builder)
		fbs.BattleActionReportAddAttacker(builder, overflowValue)
		protocol := fbs.BattleActionReportEnd(builder)

		fbs.BattleReportStartProtocolVector(builder, 1)
		builder.PrependUOffsetT(protocol)
		protocolVector := builder.EndVector(1)

		fbs.BattleReportStart(builder)
		fbs.BattleReportAddId(builder, fbs.CreateUUID(builder, 1, 2))
		fbs.BattleReportAddProtocol(builder, protocolVector)
		return fbs.BattleReportEnd(builder)
	})

	_, err := PayloadToBattleReport(payload)
	if err == nil {
		t.Fatal("expected overflow error")
	}
	if !strings.Contains(err.Error(), "overflows int") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestPayloadToBattleReportOverflowUint(t *testing.T) {
	t.Parallel()

	if strconv.IntSize == 64 {
		t.Skip("uint overflow from uint64 is not possible on 64-bit runtime")
	}

	maxUint := uint64(^uint(0))
	overflowValue := maxUint + 1
	payload := buildBattleReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
		fbs.BattleReportStart(builder)
		fbs.BattleReportAddId(builder, fbs.CreateUUID(builder, 1, 2))
		fbs.BattleReportAddPlanet(builder, overflowValue)
		return fbs.BattleReportEnd(builder)
	})

	_, err := PayloadToBattleReport(payload)
	if err == nil {
		t.Fatal("expected overflow error")
	}
	if !strings.Contains(err.Error(), "overflows uint") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestBattleReportToPayloadDeterministicMapEncoding(t *testing.T) {
	t.Parallel()

	report := sampleBattleReport()

	firstPayload, err := BattleReportToPayload(report)
	if err != nil {
		t.Fatalf("encode battle report payload: %v", err)
	}

	for i := 0; i < 20; i++ {
		nextPayload, nextErr := BattleReportToPayload(report)
		if nextErr != nil {
			t.Fatalf("encode battle report payload #%d: %v", i+2, nextErr)
		}
		if !bytes.Equal(firstPayload, nextPayload) {
			t.Fatalf("payload differs between runs at iteration %d", i+2)
		}
	}
}

func sampleBattleReport() *model.BattleReport {
	return &model.BattleReport{
		ID:         uuid.MustParse("44444444-4444-4444-4444-444444444444"),
		Planet:     12,
		PlanetName: "Nexus",
		Races: map[int]uuid.UUID{
			2: uuid.MustParse("55555555-5555-5555-5555-555555555555"),
			1: uuid.MustParse("66666666-6666-6666-6666-666666666666"),
		},
		Ships: map[int]model.BattleReportGroup{
			3: {
				InBattle:     true,
				Number:       20,
				NumberLeft:   7,
				LoadQuantity: model.Float(3.5),
				Tech:         map[string]model.Float{"WEAPONS": model.Float(2.0), "DRIVE": model.Float(1.5)},
				Race:         "Terrans",
				ClassName:    "Frigate",
				LoadType:     "MAT",
			},
			1: {
				InBattle:     false,
				Number:       15,
				NumberLeft:   15,
				LoadQuantity: model.Float(0.0),
				Tech:         map[string]model.Float{"CARGO": model.Float(1.25), "SHIELDS": model.Float(1.75)},
				Race:         "Martians",
				ClassName:    "Destroyer",
				LoadType:     "CAP",
			},
		},
		Protocol: []model.BattleActionReport{
			{Attacker: 1, AttackerShipClass: 3, Defender: 2, DefenderShipClass: 1, Destroyed: true},
			{Attacker: 2, AttackerShipClass: 1, Defender: 1, DefenderShipClass: 3, Destroyed: false},
		},
	}
}

func battleReportWireClone(t *testing.T, source *model.BattleReport) *model.BattleReport {
	t.Helper()

	data, err := source.MarshalBinary()
	if err != nil {
		t.Fatalf("marshal source battle report: %v", err)
	}

	result := new(model.BattleReport)
	if err := result.UnmarshalBinary(data); err != nil {
		t.Fatalf("unmarshal source battle report clone: %v", err)
	}

	return result
}

func buildBattleReportPayload(build func(*flatbuffers.Builder) flatbuffers.UOffsetT) []byte {
	builder := flatbuffers.NewBuilder(256)
	offset := build(builder)
	fbs.FinishBattleReportBuffer(builder, offset)
	return builder.FinishedBytes()
}
