package wire

import (
	"context"
	"fmt"
	"net"
	"testing"
	"time"

	"go.uber.org/zap/zaptest"
	"google.golang.org/protobuf/proto"
	"nhooyr.io/websocket"

	proto_socket "git.toki-labs.com/toki/proto-socket/go"
	iop "iop/proto/gen/iop"
)

func getFreePort(t *testing.T) int {
	t.Helper()
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("failed to find free port: %v", err)
	}
	defer ln.Close()
	return ln.Addr().(*net.TCPAddr).Port
}

func TestClientServerHandshake(t *testing.T) {
	logger := zaptest.NewLogger(t)
	port := getFreePort(t)
	listenAddr := fmt.Sprintf("127.0.0.1:%d", port)

	server, err := NewClientServer(listenAddr, logger)
	if err != nil {
		t.Fatalf("NewClientServer failed: %v", err)
	}

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

	if err := server.Start(ctx); err != nil {
		t.Fatalf("server Start failed: %v", err)
	}
	defer func() {
		_ = server.Stop()
	}()

	// Give a tiny moment for server to bind
	time.Sleep(50 * time.Millisecond)

	// Dial to the client endpoint
	parserMap := proto_socket.ParserMap{
		proto_socket.TypeNameOf(&iop.ClientHelloRequest{}): func(b []byte) (proto.Message, error) {
			req := &iop.ClientHelloRequest{}
			return req, proto.Unmarshal(b, req)
		},
		proto_socket.TypeNameOf(&iop.ClientHelloResponse{}): func(b []byte) (proto.Message, error) {
			res := &iop.ClientHelloResponse{}
			return res, proto.Unmarshal(b, res)
		},
	}

	// Dial using WS client
	client, err := proto_socket.DialWsWithHeartbeat(
		ctx,
		"127.0.0.1",
		port,
		"/client",
		proto_socket.DefaultHeartbeatIntervalSec,
		proto_socket.DefaultHeartbeatWaitSec,
		parserMap,
	)
	if err != nil {
		t.Fatalf("failed to DialWs: %v", err)
	}
	defer client.Close()

	// Send ClientHelloRequest and wait for ClientHelloResponse
	req := &iop.ClientHelloRequest{
		ClientId:      "test-client-id",
		ClientVersion: "1.0.0",
	}

	res, err := proto_socket.SendRequestTyped[*iop.ClientHelloRequest, *iop.ClientHelloResponse](
		&client.Communicator,
		req,
		2*time.Second,
	)
	if err != nil {
		t.Fatalf("SendRequestTyped failed: %v", err)
	}

	if !res.Ready {
		t.Errorf("expected response.Ready to be true, got false")
	}
	if res.Protocol != Protocol {
		t.Errorf("expected response.Protocol to be %q, got %q", Protocol, res.Protocol)
	}
	if res.ServerTimeUnixNano <= 0 {
		t.Errorf("expected server time unix nano to be positive, got %d", res.ServerTimeUnixNano)
	}
	if res.Message != "Hello from IOP Control Plane" {
		t.Errorf("expected message to be %q, got %q", "Hello from IOP Control Plane", res.Message)
	}
}

func TestClientServerHandshakeWithBrowserOrigin(t *testing.T) {
	logger := zaptest.NewLogger(t)
	port := getFreePort(t)
	listenAddr := fmt.Sprintf("127.0.0.1:%d", port)

	server, err := NewClientServer(listenAddr, logger)
	if err != nil {
		t.Fatalf("NewClientServer failed: %v", err)
	}

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

	if err := server.Start(ctx); err != nil {
		t.Fatalf("server Start failed: %v", err)
	}
	defer func() {
		_ = server.Stop()
	}()

	time.Sleep(50 * time.Millisecond)

	parserMap := proto_socket.ParserMap{
		proto_socket.TypeNameOf(&iop.ClientHelloRequest{}): func(b []byte) (proto.Message, error) {
			req := &iop.ClientHelloRequest{}
			return req, proto.Unmarshal(b, req)
		},
		proto_socket.TypeNameOf(&iop.ClientHelloResponse{}): func(b []byte) (proto.Message, error) {
			res := &iop.ClientHelloResponse{}
			return res, proto.Unmarshal(b, res)
		},
	}

	// Dial specifying browser Origin
	dialOpts := &websocket.DialOptions{
		HTTPHeader: map[string][]string{
			"Origin": {"http://localhost:3000"},
		},
	}
	conn, _, err := websocket.Dial(ctx, fmt.Sprintf("ws://127.0.0.1:%d/client", port), dialOpts)
	if err != nil {
		t.Fatalf("failed to Dial with Origin: %v", err)
	}

	// Wrap in WsClient
	client := proto_socket.NewWsClient(conn, proto_socket.DefaultHeartbeatIntervalSec, proto_socket.DefaultHeartbeatWaitSec, parserMap)
	defer client.Close()

	// Send ClientHelloRequest
	req := &iop.ClientHelloRequest{
		ClientId:      "browser-client",
		ClientVersion: "1.0.0",
	}

	res, err := proto_socket.SendRequestTyped[*iop.ClientHelloRequest, *iop.ClientHelloResponse](
		&client.Communicator,
		req,
		2*time.Second,
	)
	if err != nil {
		t.Fatalf("SendRequestTyped failed: %v", err)
	}

	if !res.Ready {
		t.Errorf("expected response.Ready to be true, got false")
	}
	if res.Protocol != Protocol {
		t.Errorf("expected response.Protocol to be %q, got %q", Protocol, res.Protocol)
	}
}
