package network

import (
	"bufio"
	"bytes"
	"io"
	"log/slog"
	"net"
	"sync"
	"testing"
	"time"

	"regionio/internal/protocol"
	"regionio/internal/server"
	"regionio/internal/world"
)

type recordingConn struct {
	mu  sync.Mutex
	buf bytes.Buffer
}

func (c *recordingConn) Read([]byte) (int, error)         { return 0, io.EOF }
func (c *recordingConn) Close() error                     { return nil }
func (c *recordingConn) LocalAddr() net.Addr              { return testAddr("local") }
func (c *recordingConn) RemoteAddr() net.Addr             { return testAddr("remote") }
func (c *recordingConn) SetDeadline(time.Time) error      { return nil }
func (c *recordingConn) SetReadDeadline(time.Time) error  { return nil }
func (c *recordingConn) SetWriteDeadline(time.Time) error { return nil }
func (c *recordingConn) Write(p []byte) (int, error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.buf.Write(p)
}

func (c *recordingConn) take(t *testing.T) []protocol.Packet {
	t.Helper()
	c.mu.Lock()
	raw := append([]byte(nil), c.buf.Bytes()...)
	c.buf.Reset()
	c.mu.Unlock()

	reader := bytes.NewReader(raw)
	br := bufio.NewReader(reader)
	var packets []protocol.Packet
	for br.Buffered() > 0 || reader.Len() > 0 {
		pkt, err := protocol.ReadPacket(br, -1)
		if err != nil {
			t.Fatalf("decode recorded packet: %v", err)
		}
		packets = append(packets, pkt)
	}
	return packets
}

func (c *recordingConn) countPacketID(id int32) int {
	c.mu.Lock()
	raw := append([]byte(nil), c.buf.Bytes()...)
	c.mu.Unlock()
	reader := bytes.NewReader(raw)
	br := bufio.NewReader(reader)
	count := 0
	for br.Buffered() > 0 || reader.Len() > 0 {
		packet, err := protocol.ReadPacket(br, -1)
		if err != nil {
			return count
		}
		if packet.ID == id {
			count++
		}
	}
	return count
}

type testAddr string

func (a testAddr) Network() string { return "test" }
func (a testAddr) String() string  { return string(a) }

func TestIntegrationTwoPlayersShareBlockAndChatUpdates(t *testing.T) {
	cfg := server.DefaultConfig()
	cfg.WorldDir = ""
	cfg.MaxPlayers = 4
	cache := world.NewCache(-1, world.GenerateFlat)
	srv, err := server.NewWithCache(cfg, cache)
	if err != nil {
		t.Fatal(err)
	}

	raw1, raw2 := &recordingConn{}, &recordingConn{}
	h1 := &handler{conn: NewConn(raw1), srv: srv, log: slog.Default()}
	h2 := &handler{conn: NewConn(raw2), srv: srv, log: slog.Default()}
	h1.conn.Profile = server.Profile{Name: "Alice", UUID: server.OfflineUUID("Alice")}
	h2.conn.Profile = server.Profile{Name: "Bob", UUID: server.OfflineUUID("Bob")}
	h1.session, err = srv.RegisterPlayer(h1.conn.Profile, h1.conn.Send)
	if err != nil {
		t.Fatal(err)
	}
	defer srv.UnregisterPlayer(h1.session)
	h2.session, err = srv.RegisterPlayer(h2.conn.Profile, h2.conn.Send)
	if err != nil {
		t.Fatal(err)
	}
	defer srv.UnregisterPlayer(h2.session)

	action := protocol.NewWriter(24)
	action.VarInt(0).Position(2, world.FlatSurfaceY, 3).Byte(1).VarInt(17)
	if err := h1.handlePlayerAction(protocol.Packet{ID: protocol.PlayPlayerAction, Data: action.Bytes()}); err != nil {
		t.Fatal(err)
	}
	if got := cache.GetBlock(2, world.FlatSurfaceY, 3); got != world.StateAir {
		t.Fatalf("shared block state = %d, want air", got)
	}
	assertPacketIDs(t, raw1.take(t), protocol.PlayBlockUpdate, protocol.PlayLightUpdate, protocol.PlayBlockChangedAck)
	assertPacketIDs(t, raw2.take(t), protocol.PlayBlockUpdate, protocol.PlayLightUpdate)

	chat := protocol.NewWriter(16).String("hello")
	if err := h1.handleChat(protocol.Packet{ID: protocol.PlayChatMessage, Data: chat.Bytes()}); err != nil {
		t.Fatal(err)
	}
	assertPacketIDs(t, raw1.take(t), protocol.PlaySystemChat)
	assertPacketIDs(t, raw2.take(t), protocol.PlaySystemChat)
}

func TestBoundaryEditBroadcastsEveryChangedLightChunk(t *testing.T) {
	cfg := server.DefaultConfig()
	cfg.WorldDir = ""
	cache := world.NewCache(-1, func(cx, cz int32) *world.Chunk {
		return world.NewChunk(cx, cz, world.BiomePlains)
	})
	if _, err := cache.FrameErr(0, 0); err != nil {
		t.Fatal(err)
	}
	if _, err := cache.FrameErr(1, 0); err != nil {
		t.Fatal(err)
	}
	srv, err := server.NewWithCache(cfg, cache)
	if err != nil {
		t.Fatal(err)
	}
	recorder := &recordingConn{}
	h := &handler{conn: NewConn(recorder), srv: srv, log: slog.Default()}
	h.conn.Profile = server.Profile{Name: "Alice", UUID: server.OfflineUUID("Alice")}
	h.session, err = srv.RegisterPlayer(h.conn.Profile, h.conn.Send)
	if err != nil {
		t.Fatal(err)
	}
	defer srv.UnregisterPlayer(h.session)
	srv.SetPlayerTransform(h.session, 15, 100, 8, 0, 0, true)
	srv.SetPlayerViewDistance(h.session, 2)

	valid, lightChunks := cache.SetBlockWithLight(15, 100, 8, world.StateGlowstone)
	if !valid || len(lightChunks) != 6 {
		t.Fatalf("boundary edit valid=%v light chunks=%v; want six cached neighbors", valid, lightChunks)
	}
	h.broadcastBlockUpdate(15, 100, 8, world.StateGlowstone, lightChunks)
	assertPacketIDs(t, recorder.take(t), protocol.PlayBlockUpdate,
		protocol.PlayLightUpdate, protocol.PlayLightUpdate, protocol.PlayLightUpdate,
		protocol.PlayLightUpdate, protocol.PlayLightUpdate, protocol.PlayLightUpdate)
}

func TestIntegrationFourClientsVisibilityMovementLeaveAndLight(t *testing.T) {
	cfg := server.DefaultConfig()
	cfg.WorldDir = ""
	cfg.MaxPlayers = 4
	cache := world.NewCache(-1, world.GenerateFlat)
	srv, err := server.NewWithCache(cfg, cache)
	if err != nil {
		t.Fatal(err)
	}

	names := []string{"Alice", "Bob", "Carol", "Dave"}
	positions := [][3]float64{{0, 80, 0}, {16, 80, 0}, {160, 80, 0}, {176, 80, 0}}
	handlers := make([]*handler, len(names))
	recorders := make([]*recordingConn, len(names))
	for i, name := range names {
		recorders[i] = &recordingConn{}
		h := &handler{conn: NewConn(recorders[i]), srv: srv, log: slog.Default(), viewDistance: 2}
		h.conn.Profile = server.Profile{Name: name, UUID: server.OfflineUUID(name)}
		h.session, err = srv.RegisterPlayer(h.conn.Profile, h.conn.Send)
		if err != nil {
			t.Fatalf("register %s: %v", name, err)
		}
		srv.SetPlayerTransform(h.session, positions[i][0], positions[i][1], positions[i][2], 0, 0, true)
		srv.SetPlayerViewDistance(h.session, h.viewDistance)
		handlers[i] = h
	}
	defer func() {
		for _, h := range handlers {
			srv.UnregisterPlayer(h.session)
		}
	}()

	mobID := srv.Entities().Add(&world.Entity{
		TypeID: 100, TypeName: "minecraft:pig", X: 8, Y: 80, Z: 8,
	})
	for _, h := range handlers {
		if err := h.syncVisibleEntities(); err != nil {
			t.Fatal(err)
		}
	}

	for i, recorder := range recorders {
		packets := recorder.take(t)
		if got := countPackets(packets, protocol.PlayPlayerInfoUpdate); got != 4 {
			t.Fatalf("client %s player-info count = %d, want 4", names[i], got)
		}
		wantAdds := 1
		if i < 2 {
			wantAdds = 2 // nearby player plus the pig
		}
		if got := countPackets(packets, protocol.PlayAddEntity); got != wantAdds {
			t.Fatalf("client %s add-entity count = %d, want %d", names[i], got, wantAdds)
		}
	}

	move := protocol.NewWriter(40)
	move.Float64(32).Float64(80).Float64(0).Float32(90).Float32(15).Byte(1)
	if err := handlers[1].handlePlay(protocol.Packet{ID: protocol.PlayMovePosRot, Data: move.Bytes()}); err != nil {
		t.Fatal(err)
	}
	if err := handlers[0].syncVisibleEntities(); err != nil {
		t.Fatal(err)
	}
	packets := recorders[0].take(t)
	if got := countPackets(packets, protocol.PlayTeleportEntity); got != 1 {
		t.Fatalf("nearby movement teleports = %d, want 1", got)
	}
	assertTeleportTransform(t, firstPacket(t, packets, protocol.PlayTeleportEntity), handlers[1].session.EntityID, 32, 80, 0, 90, 15, true)

	move = protocol.NewWriter(32)
	move.Float64(64).Float64(80).Float64(0).Byte(1)
	if err := handlers[1].handlePlay(protocol.Packet{ID: protocol.PlayMovePos, Data: move.Bytes()}); err != nil {
		t.Fatal(err)
	}
	if err := handlers[0].syncVisibleEntities(); err != nil {
		t.Fatal(err)
	}
	assertPacketIDs(t, recorders[0].take(t), protocol.PlayRemoveEntities)

	move = protocol.NewWriter(32)
	move.Float64(16).Float64(80).Float64(0).Byte(1)
	if err := handlers[1].handlePlay(protocol.Packet{ID: protocol.PlayMovePos, Data: move.Bytes()}); err != nil {
		t.Fatal(err)
	}
	if err := handlers[0].syncVisibleEntities(); err != nil {
		t.Fatal(err)
	}
	assertPacketIDs(t, recorders[0].take(t), protocol.PlayAddEntity)

	srv.UnregisterPlayer(handlers[1].session)
	if err := handlers[0].syncVisibleEntities(); err != nil {
		t.Fatal(err)
	}
	assertPacketIDs(t, recorders[0].take(t), protocol.PlayRemoveEntities, protocol.PlayPlayerInfoRemove)

	action := protocol.NewWriter(24)
	action.VarInt(0).Position(2, world.FlatSurfaceY, 3).Byte(1).VarInt(91)
	if err := handlers[0].handlePlayerAction(protocol.Packet{ID: protocol.PlayPlayerAction, Data: action.Bytes()}); err != nil {
		t.Fatal(err)
	}
	assertPacketIDs(t, recorders[0].take(t), protocol.PlayBlockUpdate, protocol.PlayLightUpdate, protocol.PlayBlockChangedAck)
	if packets := recorders[2].take(t); len(packets) != 0 {
		t.Fatalf("far client Carol received %d block/light packets", len(packets))
	}
	if packets := recorders[3].take(t); len(packets) != 0 {
		t.Fatalf("far client Dave received %d block/light packets", len(packets))
	}

	srv.Entities().Remove(mobID)
}

func TestEntityVisibilityWaitsForResidentChunk(t *testing.T) {
	cfg := server.DefaultConfig()
	cfg.WorldDir = ""
	srv, err := server.NewWithCache(cfg, world.NewCache(-1, world.GenerateFlat))
	if err != nil {
		t.Fatal(err)
	}
	recorder := &recordingConn{}
	h := &handler{conn: NewConn(recorder), srv: srv, log: slog.Default(), viewDistance: 2}
	h.conn.Profile = server.Profile{Name: "Alice", UUID: server.OfflineUUID("Alice")}
	h.session, err = srv.RegisterPlayer(h.conn.Profile, h.conn.Send)
	if err != nil {
		t.Fatal(err)
	}
	defer srv.UnregisterPlayer(h.session)
	srv.SetPlayerTransform(h.session, 8, 80, 8, 0, 0, true)
	h.streamer = &streamer{resident: make(map[[2]int32]bool)}
	srv.Entities().Add(&world.Entity{
		TypeID: 100, TypeName: "minecraft:pig", X: 24, Y: 80, Z: 8,
	})

	if err := h.syncVisibleEntities(); err != nil {
		t.Fatal(err)
	}
	if got := countPackets(recorder.take(t), protocol.PlayAddEntity); got != 0 {
		t.Fatalf("add-entity packets before chunk = %d, want 0", got)
	}

	h.streamer.setResident([2]int32{1, 0}, true)
	if err := h.syncVisibleEntities(); err != nil {
		t.Fatal(err)
	}
	assertPacketIDs(t, recorder.take(t), protocol.PlayAddEntity)

	h.streamer.setResident([2]int32{1, 0}, false)
	if err := h.syncVisibleEntities(); err != nil {
		t.Fatal(err)
	}
	assertPacketIDs(t, recorder.take(t), protocol.PlayRemoveEntities)
}

func countPackets(packets []protocol.Packet, id int32) int {
	count := 0
	for _, packet := range packets {
		if packet.ID == id {
			count++
		}
	}
	return count
}

func firstPacket(t *testing.T, packets []protocol.Packet, id int32) protocol.Packet {
	t.Helper()
	for _, packet := range packets {
		if packet.ID == id {
			return packet
		}
	}
	t.Fatalf("packet %#x not found", id)
	return protocol.Packet{}
}

func assertTeleportTransform(t *testing.T, packet protocol.Packet, entityID int32, x, y, z float64, yaw, pitch float32, onGround bool) {
	t.Helper()
	r := packet.Body()
	if got, err := r.VarInt(); err != nil || got != entityID {
		t.Fatalf("teleport entity = %d, %v; want %d", got, err, entityID)
	}
	for _, field := range []struct {
		name string
		want float64
	}{{"x", x}, {"y", y}, {"z", z}} {
		got, err := r.Float64()
		if err != nil || got != field.want {
			t.Fatalf("teleport %s = %v, %v; want %v", field.name, got, err, field.want)
		}
	}
	for i := 0; i < 3; i++ {
		if got, err := r.Float64(); err != nil || got != 0 {
			t.Fatalf("teleport velocity[%d] = %v, %v; want 0", i, got, err)
		}
	}
	if got, err := r.Float32(); err != nil || got != yaw {
		t.Fatalf("teleport yaw = %v, %v; want %v", got, err, yaw)
	}
	if got, err := r.Float32(); err != nil || got != pitch {
		t.Fatalf("teleport pitch = %v, %v; want %v", got, err, pitch)
	}
	if _, err := r.Int32(); err != nil {
		t.Fatal(err)
	}
	if got, err := r.Bool(); err != nil || got != onGround {
		t.Fatalf("teleport onGround = %v, %v; want %v", got, err, onGround)
	}
}

func assertPacketIDs(t *testing.T, packets []protocol.Packet, want ...int32) {
	t.Helper()
	if len(packets) != len(want) {
		ids := make([]int32, len(packets))
		for i := range packets {
			ids[i] = packets[i].ID
		}
		t.Fatalf("packet IDs = %v (count %d), want %v (count %d)", ids, len(packets), want, len(want))
	}
	for i := range want {
		if packets[i].ID != want[i] {
			t.Fatalf("packet[%d] id = %#x, want %#x", i, packets[i].ID, want[i])
		}
	}
}
