package rpc

import (
	"bufio"
	"context"
	"encoding/json"
	"errors"
	"net"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

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

	type payload struct {
		Name string `json:"name"`
	}

	got, err := DecodeParams[payload](Request{})
	if err != nil {
		t.Fatalf("DecodeParams(empty): %v", err)
	}
	if got.Name != "" {
		t.Fatalf("DecodeParams(empty) = %+v, want zero value", got)
	}

	_, err = DecodeParams[payload](Request{Params: json.RawMessage(`{"name":`)})
	if err == nil {
		t.Fatal("DecodeParams(malformed) returned nil error")
	}
}

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

	socketPath, cleanup := serveRPCOnce(t, func(conn net.Conn) {
		defer conn.Close()
		var req Request
		if err := json.NewDecoder(bufio.NewReader(conn)).Decode(&req); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		if req.Version != Version || req.Method != "ping" {
			t.Fatalf("unexpected request: %+v", req)
		}
		var params map[string]string
		if err := json.Unmarshal(req.Params, &params); err != nil {
			t.Fatalf("unmarshal params: %v", err)
		}
		if params["name"] != "devbox" {
			t.Fatalf("params = %v, want name=devbox", params)
		}
		resp, err := NewResult(map[string]string{"status": "ok"})
		if err != nil {
			t.Fatalf("NewResult: %v", err)
		}
		if err := json.NewEncoder(conn).Encode(resp); err != nil {
			t.Fatalf("encode response: %v", err)
		}
	})
	defer cleanup()

	result, err := Call[map[string]string](context.Background(), socketPath, "ping", map[string]string{"name": "devbox"})
	if err != nil {
		t.Fatalf("Call: %v", err)
	}
	if result["status"] != "ok" {
		t.Fatalf("Call() result = %v, want status=ok", result)
	}
}

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

	socketPath, cleanup := serveRPCOnce(t, func(conn net.Conn) {
		defer conn.Close()
		var req Request
		if err := json.NewDecoder(bufio.NewReader(conn)).Decode(&req); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		if err := json.NewEncoder(conn).Encode(NewError("operation_failed", "boom")); err != nil {
			t.Fatalf("encode error response: %v", err)
		}
	})
	defer cleanup()

	_, err := Call[map[string]string](context.Background(), socketPath, "ping", nil)
	if err == nil || !strings.Contains(err.Error(), "operation_failed: boom") {
		t.Fatalf("Call() error = %v, want remote error", err)
	}
}

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

	socketPath, cleanup := serveRPCOnce(t, func(conn net.Conn) {
		defer conn.Close()
		var req Request
		if err := json.NewDecoder(bufio.NewReader(conn)).Decode(&req); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		if err := json.NewEncoder(conn).Encode(NewErrorWithOpID("not_found", "vm \"foo\" not found", "op-deadbeef00ff")); err != nil {
			t.Fatalf("encode error response: %v", err)
		}
	})
	defer cleanup()

	_, err := Call[map[string]string](context.Background(), socketPath, "vm.show", nil)
	if err == nil {
		t.Fatal("Call() returned nil error")
	}
	var rpcErr *ErrorResponse
	if !errors.As(err, &rpcErr) {
		t.Fatalf("Call() error %T (%v) is not *ErrorResponse — CLI cannot read the op_id", err, err)
	}
	if rpcErr.Code != "not_found" || rpcErr.OpID != "op-deadbeef00ff" {
		t.Fatalf("typed error = %+v, want code=not_found op-deadbeef00ff", rpcErr)
	}
	// String form keeps the op_id in parens so callers that only
	// log err.Error() still surface the id.
	if got := rpcErr.Error(); !strings.Contains(got, "(op-deadbeef00ff)") {
		t.Fatalf("err.Error() = %q, want op-id suffix", got)
	}
}

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

	var seenReq Request
	socketPath, cleanup := serveRPCOnce(t, func(conn net.Conn) {
		defer conn.Close()
		if err := json.NewDecoder(bufio.NewReader(conn)).Decode(&seenReq); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		resp, _ := NewResult(map[string]string{"status": "ok"})
		_ = json.NewEncoder(conn).Encode(resp)
	})
	defer cleanup()

	ctx := WithOpID(context.Background(), "op-cafef00d1234")
	if _, err := Call[map[string]string](ctx, socketPath, "ping", nil); err != nil {
		t.Fatalf("Call: %v", err)
	}
	if seenReq.OpID != "op-cafef00d1234" {
		t.Fatalf("server saw op_id = %q, want op-cafef00d1234", seenReq.OpID)
	}
}

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

	socketPath, cleanup := serveRPCOnce(t, func(conn net.Conn) {
		defer conn.Close()
		_, _ = conn.Write([]byte("{not-json}\n"))
	})
	defer cleanup()

	_, err := Call[map[string]string](context.Background(), socketPath, "ping", nil)
	if err == nil {
		t.Fatal("Call() returned nil error for malformed response")
	}
}

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

	socketPath, cleanup := serveRPCOnce(t, func(conn net.Conn) {
		defer conn.Close()
		var req Request
		if err := json.NewDecoder(bufio.NewReader(conn)).Decode(&req); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		time.Sleep(100 * time.Millisecond)
	})
	defer cleanup()

	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
	defer cancel()
	_, err := Call[map[string]string](ctx, socketPath, "ping", nil)
	if err == nil {
		t.Fatal("Call() returned nil error for deadline")
	}
}

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

	socketPath, cleanup := serveRPCOnce(t, func(conn net.Conn) {
		defer conn.Close()
		var req Request
		if err := json.NewDecoder(bufio.NewReader(conn)).Decode(&req); err != nil {
			t.Fatalf("decode request: %v", err)
		}
		var buf [1]byte
		_, _ = conn.Read(buf[:])
	})
	defer cleanup()

	ctx, cancel := context.WithCancel(context.Background())
	go func() {
		time.Sleep(20 * time.Millisecond)
		cancel()
	}()

	_, err := Call[map[string]string](ctx, socketPath, "ping", nil)
	if !errors.Is(err, context.Canceled) {
		t.Fatalf("Call() error = %v, want context canceled", err)
	}
}

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

	socketPath, cleanup := serveRPCOnce(t, func(conn net.Conn) {
		_ = conn.Close()
	})
	defer cleanup()

	if err := WaitForSocket(socketPath, 2*time.Second); err != nil {
		t.Fatalf("WaitForSocket(success): %v", err)
	}

	err := WaitForSocket(filepath.Join(t.TempDir(), "missing.sock"), 50*time.Millisecond)
	if err == nil || !strings.Contains(err.Error(), "not ready") {
		t.Fatalf("WaitForSocket(timeout) error = %v, want timeout", err)
	}
}

func serveRPCOnce(t *testing.T, handler func(net.Conn)) (string, func()) {
	t.Helper()

	socketPath := filepath.Join(t.TempDir(), "rpc.sock")
	listener, err := net.Listen("unix", socketPath)
	if err != nil {
		t.Fatalf("Listen: %v", err)
	}
	done := make(chan struct{})
	go func() {
		defer close(done)
		conn, err := listener.Accept()
		if err != nil {
			if !errors.Is(err, net.ErrClosed) {
				t.Errorf("Accept: %v", err)
			}
			return
		}
		handler(conn)
	}()

	cleanup := func() {
		_ = listener.Close()
		<-done
	}
	return socketPath, cleanup
}
