//go:build linux

package wayland

import (
	"encoding/binary"
	"fmt"
	"image"
	"image/color"
	"image/draw"
	"io"
	"net"
	"os"
	"path/filepath"
	"sync"
	"testing"
	"time"

	"chirami/internal/platform"
	"golang.org/x/sys/unix"
)

func TestBackendContract(t *testing.T) {
	var _ platform.Backend = (*backend)(nil)
}

func TestOutputGeometryAndScale(t *testing.T) {
	out := &outputInfo{
		physW:       3840,
		physH:       2160,
		outputScale: 2,
	}

	// 1. Without xdg_output, integer scale 2
	if s := out.Scale(); s != 2.0 {
		t.Errorf("Scale() = %f, want 2.0", s)
	}
	if w := out.LogicalWidth(); w != 1920.0 {
		t.Errorf("LogicalWidth() = %f, want 1920.0", w)
	}
	if h := out.LogicalHeight(); h != 1080.0 {
		t.Errorf("LogicalHeight() = %f, want 1080.0", h)
	}

	// 2. With fractional scale (e.g. 1.5)
	out.preferredScale = 1.5
	if s := out.Scale(); s != 1.5 {
		t.Errorf("Scale() with preferred = %f, want 1.5", s)
	}

	// 3. With explicit xdg_output logical dimensions
	out.hasLogical = true
	out.logicalW = 2560
	out.logicalH = 1440
	if w := out.LogicalWidth(); w != 2560.0 {
		t.Errorf("LogicalWidth() with logical = %f, want 2560.0", w)
	}
	if h := out.LogicalHeight(); h != 1440.0 {
		t.Errorf("LogicalHeight() with logical = %f, want 1440.0", h)
	}
}

func TestWireFramingSplitHeader(t *testing.T) {
	fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
	if err != nil {
		t.Fatalf("socketpair: %v", err)
	}
	serverFile := os.NewFile(uintptr(fds[0]), "server")
	clientFile := os.NewFile(uintptr(fds[1]), "client")
	defer serverFile.Close()
	defer clientFile.Close()

	clientConn, err := net.FileConn(clientFile)
	if err != nil {
		t.Fatalf("FileConn: %v", err)
	}
	defer clientConn.Close()

	unixClient, ok := clientConn.(*net.UnixConn)
	if !ok {
		t.Fatalf("expected *net.UnixConn")
	}

	msgCh := make(chan waylandMsg, 16)
	go func() {
		buf := make([]byte, 0, 16384)
		tmp := make([]byte, 8192)
		oob := make([]byte, unix.CmsgSpace(4)*8)
		var pendingFds []int

		for {
			for len(buf) >= 8 {
				objID := binary.LittleEndian.Uint32(buf[0:4])
				opAndSize := binary.LittleEndian.Uint32(buf[4:8])
				opcode := opAndSize & 0xffff
				sz := int(opAndSize >> 16)
				if sz < 8 {
					msgCh <- waylandMsg{err: fmt.Errorf("invalid message size %d", sz)}
					return
				}
				if len(buf) < sz {
					break
				}
				recFd := -1
				if len(pendingFds) > 0 {
					recFd = pendingFds[0]
					pendingFds = pendingFds[1:]
				}
				msgData := make([]byte, sz-8)
				copy(msgData, buf[8:sz])
				buf = buf[sz:]
				msgCh <- waylandMsg{objID: objID, opcode: opcode, fd: recFd, data: msgData}
			}

			n, oobn, _, _, err := unixClient.ReadMsgUnix(tmp, oob)
			if err != nil {
				msgCh <- waylandMsg{err: err}
				return
			}
			if n == 0 && oobn == 0 {
				msgCh <- waylandMsg{err: io.EOF}
				return
			}
			if oobn > 0 {
				scms, err := unix.ParseSocketControlMessage(oob[:oobn])
				if err == nil {
					for _, scm := range scms {
						fds, err := unix.ParseUnixRights(&scm)
						if err == nil {
							pendingFds = append(pendingFds, fds...)
						}
					}
				}
			}
			if n > 0 {
				buf = append(buf, tmp[:n]...)
			}
		}
	}()

	// Send message with split header write: 4 bytes then 4 bytes + 4 bytes payload
	var fullMsg [12]byte
	binary.LittleEndian.PutUint32(fullMsg[0:4], 42)
	binary.LittleEndian.PutUint32(fullMsg[4:8], (12<<16)|1)
	binary.LittleEndian.PutUint32(fullMsg[8:12], 100)

	// Write first 4 bytes
	if _, err := serverFile.Write(fullMsg[:4]); err != nil {
		t.Fatalf("write split 1: %v", err)
	}
	time.Sleep(10 * time.Millisecond)

	// Write remaining 8 bytes
	if _, err := serverFile.Write(fullMsg[4:]); err != nil {
		t.Fatalf("write split 2: %v", err)
	}

	select {
	case m := <-msgCh:
		if m.err != nil {
			t.Fatalf("unexpected reader error: %v", m.err)
		}
		if m.objID != 42 || m.opcode != 1 {
			t.Errorf("got obj %d op %d, want 42 1", m.objID, m.opcode)
		}
		if len(m.data) != 4 || binary.LittleEndian.Uint32(m.data) != 100 {
			t.Errorf("unexpected payload: %v", m.data)
		}
	case <-time.After(500 * time.Millisecond):
		t.Fatal("timed out waiting for message after split header")
	}
}

func TestMockWaylandLifecycle(t *testing.T) {
	tmpDir, err := os.MkdirTemp("/tmp", "wltest-*")
	if err != nil {
		t.Fatalf("MkdirTemp: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	disp := "wayland-0"
	sockPath := filepath.Join(tmpDir, disp)

	listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: sockPath, Net: "unix"})
	if err != nil {
		t.Fatalf("listen: %v", err)
	}
	defer listener.Close()

	t.Setenv("XDG_RUNTIME_DIR", tmpDir)
	t.Setenv("WAYLAND_DISPLAY", disp)

	var wg sync.WaitGroup
	wg.Add(1)

	// Mock Wayland Compositor Server
	go func() {
		defer wg.Done()
		conn, err := listener.AcceptUnix()
		if err != nil {
			return
		}
		defer conn.Close()

		sendMsg := func(objID, opcode uint32, args ...any) {
			var payload []byte
			for _, arg := range args {
				switch v := arg.(type) {
				case uint32:
					var b [4]byte
					binary.LittleEndian.PutUint32(b[:], v)
					payload = append(payload, b[:]...)
				case int32:
					var b [4]byte
					binary.LittleEndian.PutUint32(b[:], uint32(v))
					payload = append(payload, b[:]...)
				case string:
					payload = append(payload, encodeString(v)...)
				}
			}
			totalLen := 8 + len(payload)
			hdr := make([]byte, 8)
			binary.LittleEndian.PutUint32(hdr[0:4], objID)
			binary.LittleEndian.PutUint32(hdr[4:8], uint32((totalLen<<16)|int(opcode)))
			_, _ = conn.Write(append(hdr, payload...))
		}

		readClientMsg := func() (uint32, uint32, []byte) {
			var hdr [8]byte
			_, err := io.ReadFull(conn, hdr[:])
			if err != nil {
				return 0, 0, nil
			}
			objID := binary.LittleEndian.Uint32(hdr[0:4])
			opAndSize := binary.LittleEndian.Uint32(hdr[4:8])
			opcode := opAndSize & 0xffff
			sz := int(opAndSize >> 16)
			var data []byte
			if sz > 8 {
				data = make([]byte, sz-8)
				_, _ = io.ReadFull(conn, data)
			}
			return objID, opcode, data
		}

		var registryID uint32
		var syncID uint32
		var pointerID uint32
		var locateSurfID uint32
		var locateLayerSurfID uint32
		var seatBindID uint32

		// 1. Initial registry & sync
		for {
			obj, op, data := readClientMsg()
			if obj == 0 {
				return
			}
			if obj == 1 && op == 1 { // get_registry
				registryID = binary.LittleEndian.Uint32(data[0:4])
			}
			if obj == 1 && op == 0 { // sync
				syncID = binary.LittleEndian.Uint32(data[0:4])
				break
			}
		}

		// Advertise globals: wl_compositor (1), wl_shm (2), wl_seat (3), zwlr_layer_shell_v1 (4), wp_viewporter (5), wl_output (6)
		sendMsg(registryID, 0, uint32(1), "wl_compositor", uint32(4))
		sendMsg(registryID, 0, uint32(2), "wl_shm", uint32(1))
		sendMsg(registryID, 0, uint32(3), "wl_seat", uint32(2))
		sendMsg(registryID, 0, uint32(4), "zwlr_layer_shell_v1", uint32(1))
		sendMsg(registryID, 0, uint32(5), "wp_viewporter", uint32(1))
		sendMsg(registryID, 0, uint32(6), "wl_output", uint32(2))

		// Initial sync done
		sendMsg(syncID, 0, uint32(0))
		sendMsg(1, 1, syncID) // delete_id

		// 2. Consume binds and output geometry
		for {
			obj, op, data := readClientMsg()
			if obj == 0 {
				return
			}
			if obj == 1 && op == 0 { // second sync
				syncID = binary.LittleEndian.Uint32(data[0:4])
				break
			}
			if obj == registryID && op == 0 {
				global := binary.LittleEndian.Uint32(data[0:4])
				bindID := binary.LittleEndian.Uint32(data[len(data)-4:])
				if global == 3 {
					seatBindID = bindID
					sendMsg(bindID, 0, uint32(1)) // capabilities: pointer
				}
				if global == 6 { // output
					sendMsg(bindID, 1, uint32(1), int32(1920), int32(1080), int32(60000))
					sendMsg(bindID, 3, int32(1))
				}
			}
			if seatBindID != 0 && obj == seatBindID && op == 0 && len(data) == 4 {
				pointerID = binary.LittleEndian.Uint32(data[0:4])
			}
		}

		// Second sync done
		sendMsg(syncID, 0, uint32(0))
		sendMsg(1, 1, syncID) // delete_id

		// Main loop for server
		locateArmed := false
		for {
			obj, op, data := readClientMsg()
			if obj == 0 {
				return
			}
			// the client asks for the pointer once it has seen the seat's
			// capabilities, which can be after the second sync
			if obj == seatBindID && op == 0 && len(data) == 4 {
				pointerID = binary.LittleEndian.Uint32(data[0:4])
				continue
			}
			// layer_shell.get_layer_surface (opcode 0 on layerShellID)
			if op == 0 && len(data) >= 20 {
				createdID := binary.LittleEndian.Uint32(data[0:4])
				targetSurf := binary.LittleEndian.Uint32(data[4:8])
				name, _, _ := decodeString(data, 16)
				if name == "chirami-locate" {
					locateLayerSurfID = createdID
					locateSurfID = targetSurf
					// Send configure on locate layer surface
					sendMsg(locateLayerSurfID, 0, uint32(1), uint32(1920), uint32(1080))
				} else if name == "chirami-popup" {
					sendMsg(createdID, 0, uint32(2), uint32(280), uint32(120))
				}
			}
			// wl_region.add(0, 0, w, h)
			if op == 1 && len(data) >= 16 {
				locateArmed = true
			}
			// wl_surface.commit
			if obj == locateSurfID && op == 6 && locateArmed {
				locateArmed = false
				// Send pointer.enter
				sendMsg(pointerID, 0, uint32(10), locateSurfID, int32(100*256), int32(200*256))
			}
		}
	}()

	runErr := Run(func(b platform.Backend) error {
		// Contract checks
		x, y, ok := b.Pointer()
		if ok || x != 0 || y != 0 {
			t.Errorf("Pointer() = (%f, %f, %v), want (0, 0, false)", x, y, ok)
		}

		// Locate
		anc, err := b.Locate()
		if err != nil {
			t.Fatalf("Locate error: %v", err)
		}
		if anc.X != 100 || anc.Y != 200 {
			t.Errorf("Anchor = (%f, %f), want (100, 200)", anc.X, anc.Y)
		}
		if anc.Screen.W != 1920 || anc.Screen.H != 1080 {
			t.Errorf("Screen = %v, want 1920x1080", anc.Screen)
		}

		// Show popup
		img := image.NewRGBA(image.Rect(0, 0, 280, 120))
		draw.Draw(img, img.Bounds(), &image.Uniform{color.RGBA{255, 0, 0, 255}}, image.Point{}, draw.Src)
		err = b.Show(platform.Popup{
			Image: img,
			X:     anc.X,
			Y:     anc.Y,
			Hit:   platform.Rect{X: 0, Y: 0, W: 280, H: 120},
		})
		if err != nil {
			t.Fatalf("Show error: %v", err)
		}

		// Hide
		b.Hide()

		return nil
	})

	if runErr != nil {
		t.Fatalf("Run returned error: %v", runErr)
	}

	listener.Close()
	wg.Wait()
}

// Growing the pool for a bigger card must resize it (opcode 2), not destroy
// it (opcode 1): the next create_buffer on a destroyed pool is a protocol
// error that kills the connection.
func TestShmPoolGrowResizes(t *testing.T) {
	fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
	if err != nil {
		t.Fatal(err)
	}
	conn := func(fd int) *net.UnixConn {
		c, err := net.FileConn(os.NewFile(uintptr(fd), "sock"))
		if err != nil {
			t.Fatal(err)
		}
		return c.(*net.UnixConn)
	}
	client, server := conn(fds[0]), conn(fds[1])
	defer client.Close()
	defer server.Close()

	next := uint32(10)
	alloc := func() uint32 { next++; return next }
	p := newShmPool(3)
	defer p.destroy(client)
	if _, _, _, err := p.getBuffer(client, 100, 100, alloc, nil); err != nil {
		t.Fatal(err)
	}
	p.handleRelease(p.slots[0].bufID)
	if _, _, _, err := p.getBuffer(client, 400, 400, alloc, nil); err != nil {
		t.Fatal(err)
	}

	client.SetReadDeadline(time.Now().Add(time.Second))
	server.SetReadDeadline(time.Now().Add(time.Second))
	buf := make([]byte, 4096)
	oob := make([]byte, 64)
	var msgs []byte
	for len(msgs) < 16+32+12+8+32 { // create_pool, create_buffer, resize, destroy buffer, create_buffer
		n, _, _, _, err := server.ReadMsgUnix(buf, oob)
		if err != nil {
			t.Fatal(err)
		}
		msgs = append(msgs, buf[:n]...)
	}
	for off := 0; off+8 <= len(msgs); {
		obj := binary.LittleEndian.Uint32(msgs[off:])
		hdr := binary.LittleEndian.Uint32(msgs[off+4:])
		size, op := int(hdr>>16), hdr&0xffff
		if obj == p.poolID && op == 1 {
			t.Fatal("the pool was destroyed while growing it")
		}
		off += size
	}
}
