package ipc

import (
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"testing"
	"time"
)

// TestSendRoundTrip spins up a real server bound to a temp socket and exchanges
// one request/response through the client.
func TestSendRoundTrip(t *testing.T) {
	sock := filepath.Join(t.TempDir(), "cliamp.sock")

	disp := &captureDispatcher{autoReply: Response{OK: true}}
	srv, err := NewServer(sock, disp)
	if err != nil {
		t.Fatalf("NewServer: %v", err)
	}
	t.Cleanup(func() { _ = srv.Close() })

	resp, err := Send(sock, Request{Cmd: "play"})
	if err != nil {
		t.Fatalf("Send: %v", err)
	}
	if !resp.OK {
		t.Errorf("resp.OK = false, want true (err=%q)", resp.Error)
	}
	if _, ok := disp.last.(PlayMsg); !ok {
		t.Errorf("server received %T, want PlayMsg", disp.last)
	}
}

func TestSendNoServer(t *testing.T) {
	sock := filepath.Join(t.TempDir(), "missing.sock")

	_, err := Send(sock, Request{Cmd: "status"})
	if err == nil {
		t.Fatal("Send to missing socket should error")
	}
	if !strings.Contains(err.Error(), "not running") {
		t.Errorf("error = %q, want to mention 'not running'", err.Error())
	}
}

func TestSendInvalidRequestReturnsError(t *testing.T) {
	// Server responds to an unknown cmd with OK:false, Error:"unknown command:...".
	sock := filepath.Join(t.TempDir(), "cliamp.sock")
	srv, err := NewServer(sock, &captureDispatcher{})
	if err != nil {
		t.Fatalf("NewServer: %v", err)
	}
	t.Cleanup(func() { _ = srv.Close() })

	resp, err := Send(sock, Request{Cmd: "doesnotexist"})
	if err != nil {
		t.Fatalf("Send: %v", err)
	}
	if resp.OK {
		t.Error("unknown cmd should return !OK")
	}
	if !strings.Contains(resp.Error, "unknown command") {
		t.Errorf("error = %q, want to mention 'unknown command'", resp.Error)
	}
}

func TestDefaultSocketPath(t *testing.T) {
	t.Setenv("HOME", t.TempDir())
	p := DefaultSocketPath()
	if !strings.HasSuffix(p, filepath.Join("cliamp", "cliamp.sock")) {
		t.Errorf("DefaultSocketPath = %q, want to end with cliamp/cliamp.sock", p)
	}
}

func TestNewServerRemovesOrphanSocket(t *testing.T) {
	dir := t.TempDir()
	sock := filepath.Join(dir, "cliamp.sock")

	// Create an orphan socket file with no PID file — NewServer should remove it.
	if err := os.WriteFile(sock, []byte(""), 0o600); err != nil {
		t.Fatalf("WriteFile: %v", err)
	}

	srv, err := NewServer(sock, &captureDispatcher{})
	if err != nil {
		t.Fatalf("NewServer: %v", err)
	}
	t.Cleanup(func() { _ = srv.Close() })

	// Server is up and serving.
	resp, err := Send(sock, Request{Cmd: "play"})
	if err != nil {
		t.Fatalf("Send: %v", err)
	}
	if !resp.OK {
		t.Errorf("resp.OK = false, want true")
	}
}

func TestNewServerCorruptPIDFile(t *testing.T) {
	dir := t.TempDir()
	sock := filepath.Join(dir, "cliamp.sock")

	// Corrupt PID file is cleaned and NewServer succeeds.
	if err := os.WriteFile(sock+".pid", []byte("notanumber"), 0o600); err != nil {
		t.Fatalf("WriteFile: %v", err)
	}

	srv, err := NewServer(sock, &captureDispatcher{})
	if err != nil {
		t.Fatalf("NewServer with corrupt PID: %v", err)
	}
	_ = srv.Close()
}

func TestNewServerDeadPIDFile(t *testing.T) {
	dir := t.TempDir()
	sock := filepath.Join(dir, "cliamp.sock")

	// PID 1 is init (alive), but a far-out-of-range PID should be dead on Linux.
	// Pick a PID unlikely to exist (>2^30 PIDs don't normally exist on Linux).
	deadPID := 0x3FFFFFFF
	if err := os.WriteFile(sock+".pid", []byte(strconv.Itoa(deadPID)), 0o600); err != nil {
		t.Fatalf("WriteFile: %v", err)
	}

	srv, err := NewServer(sock, &captureDispatcher{})
	if err != nil {
		t.Fatalf("NewServer with dead PID: %v", err)
	}
	_ = srv.Close()
}

func TestNewServerLivePIDReturnsError(t *testing.T) {
	dir := t.TempDir()
	sock := filepath.Join(dir, "cliamp.sock")

	// Our own PID is definitely live → NewServer should refuse to start.
	if err := os.WriteFile(sock+".pid", []byte(strconv.Itoa(os.Getpid())), 0o600); err != nil {
		t.Fatalf("WriteFile: %v", err)
	}

	srv, err := NewServer(sock, &captureDispatcher{})
	if err == nil {
		_ = srv.Close()
		t.Fatal("NewServer should error when PID file contains a live process")
	}
	if !strings.Contains(err.Error(), "already running") {
		t.Errorf("error = %q, want to mention 'already running'", err.Error())
	}
}

func TestServerCloseRemovesFiles(t *testing.T) {
	sock := filepath.Join(t.TempDir(), "cliamp.sock")
	srv, err := NewServer(sock, &captureDispatcher{})
	if err != nil {
		t.Fatalf("NewServer: %v", err)
	}

	if _, err := os.Stat(sock); err != nil {
		t.Fatalf("socket should exist after NewServer: %v", err)
	}
	if _, err := os.Stat(sock + ".pid"); err != nil {
		t.Fatalf("pid file should exist after NewServer: %v", err)
	}

	if err := srv.Close(); err != nil {
		t.Fatalf("Close: %v", err)
	}

	if _, err := os.Stat(sock); !os.IsNotExist(err) {
		t.Errorf("socket still exists after Close: %v", err)
	}
	if _, err := os.Stat(sock + ".pid"); !os.IsNotExist(err) {
		t.Errorf("pid file still exists after Close: %v", err)
	}
}

func TestServerMultipleRequestsSameConnection(t *testing.T) {
	// Make sure the server can handle multiple requests over a single socket.
	// Each Send opens its own connection, so this really verifies the accept
	// loop keeps going beyond the first request.
	sock := filepath.Join(t.TempDir(), "cliamp.sock")
	disp := &captureDispatcher{}
	srv, err := NewServer(sock, disp)
	if err != nil {
		t.Fatalf("NewServer: %v", err)
	}
	t.Cleanup(func() { _ = srv.Close() })

	for _, cmd := range []string{"play", "pause", "next", "prev"} {
		resp, err := Send(sock, Request{Cmd: cmd})
		if err != nil {
			t.Fatalf("Send %s: %v", cmd, err)
		}
		if !resp.OK {
			t.Errorf("cmd %s OK=false, err=%q", cmd, resp.Error)
		}
	}
}

func TestServerHandlesInvalidJSON(t *testing.T) {
	sock := filepath.Join(t.TempDir(), "cliamp.sock")
	srv, err := NewServer(sock, &captureDispatcher{})
	if err != nil {
		t.Fatalf("NewServer: %v", err)
	}
	t.Cleanup(func() { _ = srv.Close() })

	// Connect raw, send garbage, read response line.
	conn, err := dialWithTimeout(sock, time.Second)
	if err != nil {
		t.Fatalf("dial: %v", err)
	}
	defer conn.Close()

	if _, err := conn.Write([]byte("not valid json\n")); err != nil {
		t.Fatalf("write: %v", err)
	}
	buf := make([]byte, 512)
	_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
	n, err := conn.Read(buf)
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	if !strings.Contains(string(buf[:n]), "invalid JSON") {
		t.Errorf("response = %q, want to mention 'invalid JSON'", string(buf[:n]))
	}
}
