package transcoder

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

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

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

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

	source := &model.GameReportRequest{
		GameID: uuid.MustParse("11111111-2222-3333-4444-555555555555"),
		Turn:   42,
	}

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

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

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

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

	if _, err := GameReportRequestToPayload(nil); err == nil {
		t.Fatalf("expected error encoding nil request")
	}
	if _, err := PayloadToGameReportRequest(nil); err == nil {
		t.Fatalf("expected error decoding empty payload")
	}
}

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

	source := sampleReport()

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

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

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

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

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

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

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

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

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

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

	payload := buildReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
		class := builder.CreateString("frigate")
		state := builder.CreateString("orbit")

		fbs.LocalGroupStart(builder)
		fbs.LocalGroupAddClass(builder, class)
		fbs.LocalGroupAddState(builder, state)
		localGroup := fbs.LocalGroupEnd(builder)

		fbs.ReportStartLocalGroupVector(builder, 1)
		builder.PrependUOffsetT(localGroup)
		localGroups := builder.EndVector(1)

		fbs.ReportStart(builder)
		fbs.ReportAddLocalGroup(builder, localGroups)
		return fbs.ReportEnd(builder)
	})

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

func TestPayloadToReportOverflow(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 := buildReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
		fbs.ReportStart(builder)
		fbs.ReportAddTurn(builder, overflowValue)
		return fbs.ReportEnd(builder)
	})

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

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

	report := sampleReport()

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

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

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

	source := &model.Report{
		Race:  "Terrans",
		Votes: model.Float(0.123456789),
		LocalScience: []model.Science{
			{
				Name:  "science-alpha",
				Drive: model.Float(0.123456789),
			},
		},
	}

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

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

	wantVotes := model.Float(float64(float32(source.Votes.F())))
	if decoded.Votes != wantVotes {
		t.Fatalf("unexpected votes value: got=%v want=%v", decoded.Votes, wantVotes)
	}

	wantDrive := model.Float(float64(float32(source.LocalScience[0].Drive.F())))
	if decoded.LocalScience[0].Drive != wantDrive {
		t.Fatalf("unexpected drive value: got=%v want=%v", decoded.LocalScience[0].Drive, wantDrive)
	}

	if decoded.Votes == source.Votes {
		t.Fatal("expected quantization for votes value")
	}
}

func sampleReport() *model.Report {
	originA := uint(11)
	originB := uint(17)
	rangeA := model.Float(6.5)
	rangeB := model.Float(3.5)
	fleetName := "Fleet-1"

	return &model.Report{
		Version:     2,
		Turn:        7,
		Width:       64,
		Height:      48,
		PlanetCount: 21,
		Race:        "Terrans",
		RaceID:      uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
		Votes:       model.Float(7.5),
		VoteFor:     "Martians",
		Player: []model.Player{
			{
				ID:         uuid.MustParse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
				Name:       "Terrans",
				Drive:      model.Float(1.5),
				Weapons:    model.Float(2.0),
				Shields:    model.Float(2.5),
				Cargo:      model.Float(3.0),
				Population: model.Float(120.0),
				Industry:   model.Float(90.0),
				Planets:    5,
				Relation:   "-",
				Votes:      model.Float(7.5),
				Extinct:    false,
			},
		},
		LocalScience: []model.Science{
			{Name: "fusion", Drive: model.Float(1.25), Weapons: model.Float(1.5), Shields: model.Float(1.75), Cargo: model.Float(2.0)},
		},
		OtherScience: []model.OtherScience{
			{Race: "Martians", Science: model.Science{Name: "warp", Drive: model.Float(2.0), Weapons: model.Float(2.25), Shields: model.Float(2.5), Cargo: model.Float(2.75)}},
		},
		LocalShipClass: []model.ShipClass{
			{Name: "frigate", Drive: model.Float(1.5), Armament: 4, Weapons: model.Float(2.0), Shields: model.Float(2.5), Cargo: model.Float(3.0), Mass: model.Float(9.5)},
		},
		OtherShipClass: []model.OthersShipClass{
			{Race: "Martians", ShipClass: model.ShipClass{Name: "destroyer", Drive: model.Float(1.75), Armament: 6, Weapons: model.Float(2.25), Shields: model.Float(2.75), Cargo: model.Float(3.25), Mass: model.Float(10.5)}},
		},
		Battle: []model.BattleSummary{
			{
				ID:     uuid.MustParse("11111111-1111-1111-1111-111111111111"),
				Planet: 4,
				Shots:  17,
			},
			{
				ID:     uuid.MustParse("22222222-2222-2222-2222-222222222222"),
				Planet: 11,
				Shots:  3,
			},
		},
		Bombing: []*model.Bombing{
			{
				PlanetOwnedID: uuid.MustParse("cccccccc-cccc-cccc-cccc-cccccccccccc"),
				Number:        9,
				Planet:        "Nova",
				Owner:         "Terrans",
				Attacker:      "Martians",
				Production:    "SHIP",
				Industry:      model.Float(10.5),
				Population:    model.Float(8.5),
				Colonists:     model.Float(7.5),
				Capital:       model.Float(6.5),
				Material:      model.Float(5.5),
				AttackPower:   model.Float(4.5),
				Wiped:         false,
			},
		},
		IncomingGroup: []model.IncomingGroup{
			{Origin: 1, Destination: 2, Distance: model.Float(10.0), Speed: model.Float(2.0), Mass: model.Float(20.0)},
		},
		LocalPlanet: []model.LocalPlanet{
			{
				UninhabitedPlanet: model.UninhabitedPlanet{
					UnidentifiedPlanet: model.UnidentifiedPlanet{X: model.Float(1.0), Y: model.Float(2.0), Number: 3},
					Size:               model.Float(50.0),
					Name:               "Terra",
					Resources:          model.Float(7.0),
					Capital:            model.Float(8.0),
					Material:           model.Float(9.0),
				},
				Industry:     model.Float(10.0),
				Population:   model.Float(11.0),
				Colonists:    model.Float(12.0),
				Production:   "SHIP",
				FreeIndustry: model.Float(13.0),
			},
		},
		ShipProduction: []model.ShipProduction{
			{Planet: 3, Class: "frigate", Cost: model.Float(4.0), ProdUsed: model.Float(2.0), Percent: model.Float(50.0), Free: model.Float(6.0)},
		},
		Route: []model.Route{
			{Planet: 3, Route: map[uint]string{9: "MAT", 2: "CAP", 5: "EMP"}},
		},
		OtherPlanet: []model.OtherPlanet{
			{
				Owner: "Martians",
				LocalPlanet: model.LocalPlanet{
					UninhabitedPlanet: model.UninhabitedPlanet{
						UnidentifiedPlanet: model.UnidentifiedPlanet{X: model.Float(4.0), Y: model.Float(5.0), Number: 6},
						Size:               model.Float(40.0),
						Name:               "Ares",
						Resources:          model.Float(6.0),
						Capital:            model.Float(7.0),
						Material:           model.Float(8.0),
					},
					Industry:     model.Float(9.0),
					Population:   model.Float(10.0),
					Colonists:    model.Float(11.0),
					Production:   "MAT",
					FreeIndustry: model.Float(12.0),
				},
			},
		},
		UninhabitedPlanet: []model.UninhabitedPlanet{
			{UnidentifiedPlanet: model.UnidentifiedPlanet{X: model.Float(6.0), Y: model.Float(7.0), Number: 8}, Size: model.Float(30.0), Name: "Nadir", Resources: model.Float(5.0), Capital: model.Float(4.0), Material: model.Float(3.0)},
		},
		UnidentifiedPlanet: []model.UnidentifiedPlanet{
			{X: model.Float(8.0), Y: model.Float(9.0), Number: 10},
		},
		LocalFleet: []model.LocalFleet{
			{Name: "Fleet-1", Groups: 2, Destination: 4, Origin: &originA, Range: &rangeA, Speed: model.Float(2.0), State: "moving"},
		},
		LocalGroup: []model.LocalGroup{
			{
				OtherGroup: model.OtherGroup{
					Number:      1,
					Class:       "frigate",
					Tech:        map[string]model.Float{"WEAPONS": model.Float(2.0), "DRIVE": model.Float(1.5), "SHIELDS": model.Float(1.75)},
					Cargo:       "MAT",
					Load:        model.Float(4.0),
					Destination: 4,
					Origin:      &originB,
					Range:       &rangeB,
					Speed:       model.Float(2.5),
					Mass:        model.Float(12.0),
				},
				ID:    uuid.MustParse("33333333-3333-3333-3333-333333333333"),
				State: "in_orbit",
				Fleet: &fleetName,
			},
		},
		OtherGroup: []model.OtherGroup{
			{Number: 2, Class: "scout", Tech: map[string]model.Float{"CARGO": model.Float(1.25), "DRIVE": model.Float(1.75)}, Cargo: "CAP", Load: model.Float(3.5), Destination: 5, Speed: model.Float(2.25), Mass: model.Float(8.5)},
		},
		UnidentifiedGroup: []model.UnidentifiedGroup{
			{X: model.Float(10.0), Y: model.Float(11.0)},
		},
		OnPlanetGroupCache: map[uint][]int{
			1: {2, 3},
		},
		InSpaceGroupRangeCache: map[int]map[uint]float64{
			1: {4: 12.5},
		},
	}
}

func reportWireClone(t *testing.T, source *model.Report) *model.Report {
	t.Helper()

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

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

	return result
}

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