//go:build linux

package wayland

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"fmt"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"testing"
	"time"

	"chirami/internal/platform"
)

var testPNG = append([]byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a}, []byte("synthetic-png-payload-for-testing")...)

func newTestBackend(ctx context.Context) *backend {
	return &backend{
		shots:           make(chan []byte, 16),
		ctx:             ctx,
		clipImgCh:       make(chan clipImgReq),
		clipboardNotify: make(chan struct{}, 1),
	}
}

// TestNiriEventParsing_Path verifies that a ScreenshotCaptured event with a valid
// file path reads the file directly and delivers it on Screenshots().
func TestNiriEventParsing_Path(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	sockDir := t.TempDir()
	sockPath := filepath.Join(sockDir, "niri.sock")
	t.Setenv("NIRI_SOCKET", sockPath)

	l, err := net.Listen("unix", sockPath)
	if err != nil {
		t.Fatalf("listen unix: %v", err)
	}
	defer l.Close()

	// Write synthetic screenshot file
	shotFile := filepath.Join(sockDir, "shot.png")
	if err := os.WriteFile(shotFile, testPNG, 0600); err != nil {
		t.Fatalf("write shot file: %v", err)
	}

	b := newTestBackend(ctx)

	go func() {
		conn, err := l.Accept()
		if err != nil {
			return
		}
		defer conn.Close()

		r := bufio.NewReader(conn)
		req, err := r.ReadString('\n')
		if err != nil || req != "\"EventStream\"\n" {
			t.Errorf("server received unexpected request: %q, err: %v", req, err)
			return
		}

		// Send handshake reply
		_, _ = conn.Write([]byte("{\"Ok\":\"Handled\"}\n"))

		// Send ScreenshotCaptured event with file path
		ev := fmt.Sprintf("{\"ScreenshotCaptured\":{\"path\":%q}}\n", shotFile)
		_, _ = conn.Write([]byte(ev))
	}()

	shots := b.Screenshots()

	select {
	case got := <-shots:
		if !bytes.Equal(got, testPNG) {
			t.Fatalf("received %q, want %q", got, testPNG)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timed out waiting for screenshot on Screenshots()")
	}
}

// TestNiriEventParsing_Clipboard verifies that a ScreenshotCaptured event with path: null
// reads image/png from the clipboard and delivers it on Screenshots().
func TestNiriEventParsing_Clipboard(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	sockDir := t.TempDir()
	sockPath := filepath.Join(sockDir, "niri.sock")
	t.Setenv("NIRI_SOCKET", sockPath)

	l, err := net.Listen("unix", sockPath)
	if err != nil {
		t.Fatalf("listen unix: %v", err)
	}
	defer l.Close()

	b := newTestBackend(ctx)
	// Mark recent clipboard offer timestamp so it does not wait
	b.lastClipOfferNanos.Store(time.Now().UnixNano())

	// Mock clipboard responder
	go func() {
		for {
			select {
			case <-ctx.Done():
				return
			case req := <-b.clipImgCh:
				req.resp <- testPNG
			}
		}
	}()

	go func() {
		conn, err := l.Accept()
		if err != nil {
			return
		}
		defer conn.Close()

		r := bufio.NewReader(conn)
		req, err := r.ReadString('\n')
		if err != nil || req != "\"EventStream\"\n" {
			return
		}

		_, _ = conn.Write([]byte("{\"Ok\":\"Handled\"}\n"))
		_, _ = conn.Write([]byte("{\"ScreenshotCaptured\":{\"path\":null}}\n"))
	}()

	shots := b.Screenshots()

	select {
	case got := <-shots:
		if !bytes.Equal(got, testPNG) {
			t.Fatalf("received %q, want %q", got, testPNG)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timed out waiting for clipboard screenshot")
	}
}

// TestNiriPathFallbackToClipboard verifies that when path is non-existent,
// it falls back to reading image/png from clipboard.
func TestNiriPathFallbackToClipboard(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	sockDir := t.TempDir()
	sockPath := filepath.Join(sockDir, "niri.sock")
	t.Setenv("NIRI_SOCKET", sockPath)

	l, err := net.Listen("unix", sockPath)
	if err != nil {
		t.Fatalf("listen unix: %v", err)
	}
	defer l.Close()

	b := newTestBackend(ctx)
	b.lastClipOfferNanos.Store(time.Now().UnixNano())

	go func() {
		for {
			select {
			case <-ctx.Done():
				return
			case req := <-b.clipImgCh:
				req.resp <- testPNG
			}
		}
	}()

	go func() {
		conn, err := l.Accept()
		if err != nil {
			return
		}
		defer conn.Close()

		r := bufio.NewReader(conn)
		_, _ = r.ReadString('\n')
		_, _ = conn.Write([]byte("{\"Ok\":\"Handled\"}\n"))
		// Non-existent path
		_, _ = conn.Write([]byte("{\"ScreenshotCaptured\":{\"path\":\"/tmp/definitely_not_a_screenshot_chirami.png\"}}\n"))
	}()

	shots := b.Screenshots()

	select {
	case got := <-shots:
		if !bytes.Equal(got, testPNG) {
			t.Fatalf("received %q, want %q", got, testPNG)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timed out waiting for fallback clipboard screenshot")
	}
}

// TestNiriUnrelatedEvents verifies that other events from niri EventStream
// are safely ignored without error or delivering fake screenshots.
func TestNiriUnrelatedEvents(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	sockDir := t.TempDir()
	sockPath := filepath.Join(sockDir, "niri.sock")
	t.Setenv("NIRI_SOCKET", sockPath)

	l, err := net.Listen("unix", sockPath)
	if err != nil {
		t.Fatalf("listen unix: %v", err)
	}
	defer l.Close()

	shotFile := filepath.Join(sockDir, "shot.png")
	if err := os.WriteFile(shotFile, testPNG, 0600); err != nil {
		t.Fatalf("write shot file: %v", err)
	}

	b := newTestBackend(ctx)

	go func() {
		conn, err := l.Accept()
		if err != nil {
			return
		}
		defer conn.Close()

		r := bufio.NewReader(conn)
		_, _ = r.ReadString('\n')
		_, _ = conn.Write([]byte("{\"Ok\":\"Handled\"}\n"))

		// Send arbitrary niri events before the screenshot event
		unrelated := []string{
			"{\"WorkspacesChanged\":{\"workspaces\":[{\"id\":1,\"name\":\"1\",\"is_active\":true}]}}\n",
			"{\"WorkspaceUrgencyChanged\":{\"id\":1,\"urgent\":false}}\n",
			"{\"WindowOpenedOrChanged\":{\"window\":{\"id\":42,\"title\":\"browser\",\"app_id\":\"zen\"}}}\"\n",
			"{\"ConfigLoaded\":{\"failed\":false}}\n",
		}
		for _, u := range unrelated {
			_, _ = conn.Write([]byte(u))
		}

		// Now send the actual screenshot
		ev := fmt.Sprintf("{\"ScreenshotCaptured\":{\"path\":%q}}\n", shotFile)
		_, _ = conn.Write([]byte(ev))
	}()

	shots := b.Screenshots()

	select {
	case got := <-shots:
		if !bytes.Equal(got, testPNG) {
			t.Fatalf("received %q, want %q", got, testPNG)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timed out waiting for screenshot after unrelated events")
	}
}

// TestNiriReconnect verifies that if niri restarts / drops the IPC connection,
// the watcher reconnects with backoff and resumes receiving events.
func TestNiriReconnect(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
	defer cancel()

	sockDir := t.TempDir()
	sockPath := filepath.Join(sockDir, "niri.sock")
	t.Setenv("NIRI_SOCKET", sockPath)

	l, err := net.Listen("unix", sockPath)
	if err != nil {
		t.Fatalf("listen unix: %v", err)
	}
	defer l.Close()

	shotFile := filepath.Join(sockDir, "reconnect_shot.png")
	if err := os.WriteFile(shotFile, testPNG, 0600); err != nil {
		t.Fatalf("write shot file: %v", err)
	}

	b := newTestBackend(ctx)

	go func() {
		// Connection 1: handshake and immediately close to simulate restart
		conn1, err := l.Accept()
		if err != nil {
			return
		}
		r1 := bufio.NewReader(conn1)
		_, _ = r1.ReadString('\n')
		_, _ = conn1.Write([]byte("{\"Ok\":\"Handled\"}\n"))
		_ = conn1.Close()

		// Connection 2: handshake and deliver screenshot
		conn2, err := l.Accept()
		if err != nil {
			return
		}
		defer conn2.Close()
		r2 := bufio.NewReader(conn2)
		_, _ = r2.ReadString('\n')
		_, _ = conn2.Write([]byte("{\"Ok\":\"Handled\"}\n"))

		ev := fmt.Sprintf("{\"ScreenshotCaptured\":{\"path\":%q}}\n", shotFile)
		_, _ = conn2.Write([]byte(ev))
	}()

	shots := b.Screenshots()

	select {
	case got := <-shots:
		if !bytes.Equal(got, testPNG) {
			t.Fatalf("received %q, want %q", got, testPNG)
		}
	case <-time.After(3 * time.Second):
		t.Fatal("timed out waiting for screenshot after reconnect")
	}
}

// TestNiriAbsent verifies that when NIRI_SOCKET is not set, Screenshots()
// simply returns an idle channel and starts no background goroutine.
func TestNiriAbsent(t *testing.T) {
	t.Setenv("NIRI_SOCKET", "")

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	b := newTestBackend(ctx)
	ch := b.Screenshots()
	if ch == nil {
		t.Fatal("Screenshots() returned nil channel")
	}

	// Verify channel remains empty
	select {
	case got := <-ch:
		t.Fatalf("unexpected screenshot on idle channel: %v", got)
	case <-time.After(100 * time.Millisecond):
		// Expected: nothing sent
	}
}

// TestLiveClipboardImage tests reading a synthetic PNG from the Wayland clipboard via
// data-control selection in a live compositor (headless sway).
// Only runs when TEST_LIVE_WAYLAND=1 is set.
func TestLiveClipboardImage(t *testing.T) {
	if os.Getenv("TEST_LIVE_WAYLAND") != "1" {
		t.Skip("skipping live wayland clipboard test; set TEST_LIVE_WAYLAND=1 to run")
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	// Start wl-copy in foreground mode to hold synthetic PNG on clipboard
	var copyErr bytes.Buffer
	copyCmd := exec.CommandContext(ctx, "wl-copy", "-f", "--type", "image/png")
	copyCmd.Stdin = bytes.NewReader(testPNG)
	copyCmd.Stderr = &copyErr
	if err := copyCmd.Start(); err != nil {
		t.Fatalf("start wl-copy: %v", err)
	}
	defer func() {
		if copyCmd.Process != nil {
			_ = copyCmd.Process.Kill()
			_ = copyCmd.Wait()
		}
	}()

	// Wait briefly for wl-copy to advertise the offer
	time.Sleep(200 * time.Millisecond)

	err := Run(func(bp platform.Backend) error {
		wb, ok := bp.(*backend)
		if !ok {
			return errors.New("backend is not *backend")
		}

		// Read clipboard image using data-control selection with settling retry
		img, err := wb.ReadClipboardImageWithRetry(ctx, 3*time.Second)
		if err != nil {
			return fmt.Errorf("ReadClipboardImage failed: %w", err)
		}
		if !bytes.Equal(img, testPNG) {
			return fmt.Errorf("got %d bytes (%q), want %d bytes (%q)", len(img), img, len(testPNG), testPNG)
		}
		return nil
	})
	if err != nil {
		t.Fatalf("Run failed: %v", err)
	}
}

// An image copied before the screenshot is never taken for it: only a
// selection that arrives around the event is read.
func TestClipboardStaleImageIgnored(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	b := newTestBackend(ctx)
	b.lastClipOfferNanos.Store(time.Now().Add(-10 * time.Second).UnixNano())
	go func() {
		for {
			select {
			case <-ctx.Done():
				return
			case req := <-b.clipImgCh:
				req.resp <- testPNG
			}
		}
	}()
	if _, err := b.ReadClipboardImageWithRetry(ctx, 200*time.Millisecond); err == nil {
		t.Fatal("read an image copied before the screenshot")
	}

	// the screenshot's selection lands a moment after the event
	go func() {
		time.Sleep(100 * time.Millisecond)
		b.lastClipOfferNanos.Store(time.Now().UnixNano())
		b.clipboardNotify <- struct{}{}
	}()
	if _, err := b.ReadClipboardImageWithRetry(ctx, time.Second); err != nil {
		t.Fatalf("new selection: %v", err)
	}
}
