package transport_test

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

	toki "git.toki-labs.com/toki/proto-socket/go"
	"go.uber.org/zap"
	"google.golang.org/protobuf/proto"

	"iop/apps/node/internal/transport"
	iop "iop/proto/gen/iop"
)

type noopHandler struct{}

func (h *noopHandler) OnRunRequest(_ context.Context, _ *transport.Session, _ *iop.RunRequest) error {
	return nil
}
func (h *noopHandler) OnCancel(_ context.Context, _ *transport.Session, _ *iop.CancelRequest) error {
	return nil
}
func (h *noopHandler) OnCommandRequest(_ context.Context, _ *transport.Session, _ *iop.NodeCommandRequest) (*iop.NodeCommandResponse, error) {
	return nil, nil
}
func (h *noopHandler) OnConfigRefresh(_ context.Context, _ *transport.Session, req *iop.NodeConfigRefreshRequest) (*iop.NodeConfigRefreshResponse, error) {
	return &iop.NodeConfigRefreshResponse{
		RequestId: req.GetRequestId(),
		Status:    iop.NodeConfigRefreshStatus_NODE_CONFIG_REFRESH_STATUS_APPLIED,
	}, nil
}
func (h *noopHandler) OnProviderTunnelRequest(_ context.Context, _ *transport.Session, _ *iop.ProviderTunnelRequest) error {
	return nil
}

func TestSession_SetHandler_ConcurrentSafe(t *testing.T) {
	var s transport.Session
	var wg sync.WaitGroup
	h := &noopHandler{}
	for i := 0; i < 50; i++ {
		wg.Add(1)
		go func() {
			t.Helper()
			defer wg.Done()
			s.SetHandler(h)
		}()
	}
	wg.Wait()
}

// buildSessionTestPipe creates a net.Pipe-based pair: one side acts as "edge"
// (sends requests) and the other side acts as the node session under test.
// The edge side parser map must include the response type; the node side must
// include the request type (handled by nodeParserMap via DialEdge, but for
// unit tests we wire it manually).
func buildSessionTestPipe(t *testing.T) (edgeSide *toki.TcpClient, nodeSide *toki.TcpClient) {
	t.Helper()
	edgeConn, nodeConn := net.Pipe()
	edgeParserMap := toki.ParserMap{
		toki.TypeNameOf(&iop.NodeConfigRefreshResponse{}): func(b []byte) (proto.Message, error) {
			m := &iop.NodeConfigRefreshResponse{}
			return m, proto.Unmarshal(b, m)
		},
		toki.TypeNameOf(&iop.ProviderTunnelFrame{}): func(b []byte) (proto.Message, error) {
			m := &iop.ProviderTunnelFrame{}
			return m, proto.Unmarshal(b, m)
		},
	}
	nodeParserMap := toki.ParserMap{
		toki.TypeNameOf(&iop.NodeConfigRefreshRequest{}): func(b []byte) (proto.Message, error) {
			m := &iop.NodeConfigRefreshRequest{}
			return m, proto.Unmarshal(b, m)
		},
		toki.TypeNameOf(&iop.ProviderTunnelRequest{}): func(b []byte) (proto.Message, error) {
			m := &iop.ProviderTunnelRequest{}
			return m, proto.Unmarshal(b, m)
		},
	}
	edgeSide = toki.NewTcpClient(edgeConn, 0, 0, edgeParserMap)
	nodeSide = toki.NewTcpClient(nodeConn, 0, 0, nodeParserMap)
	t.Cleanup(func() { edgeSide.Close(); nodeSide.Close() })
	return edgeSide, nodeSide
}

// appliedHandler always returns applied.
type appliedHandler struct{ noopHandler }

func (h *appliedHandler) OnConfigRefresh(_ context.Context, _ *transport.Session, req *iop.NodeConfigRefreshRequest) (*iop.NodeConfigRefreshResponse, error) {
	return &iop.NodeConfigRefreshResponse{
		RequestId: req.GetRequestId(),
		Status:    iop.NodeConfigRefreshStatus_NODE_CONFIG_REFRESH_STATUS_APPLIED,
	}, nil
}

// errorHandler returns an error from OnConfigRefresh.
type errorHandler struct{ noopHandler }

func (h *errorHandler) OnConfigRefresh(_ context.Context, _ *transport.Session, req *iop.NodeConfigRefreshRequest) (*iop.NodeConfigRefreshResponse, error) {
	return nil, errors.New("refresh failed")
}

// TestSessionConfigRefreshRequestReturnsHandlerResponse verifies that a
// NodeConfigRefreshRequest pushed by the edge reaches the handler and the
// response is returned to the edge.
func TestSessionConfigRefreshRequestReturnsHandlerResponse(t *testing.T) {
	edgeSide, nodeSide := buildSessionTestPipe(t)

	sess := transport.ExportNewSession(nodeSide, zap.NewNop(), "node-test", "alias-test")
	sess.SetHandler(&appliedHandler{})

	resp, err := toki.SendRequestTyped[*iop.NodeConfigRefreshRequest, *iop.NodeConfigRefreshResponse](
		&edgeSide.Communicator,
		&iop.NodeConfigRefreshRequest{RequestId: "req-1", ChangedPaths: []string{"nodes.0.providers.0.capacity"}},
		2*time.Second,
	)
	if err != nil {
		t.Fatalf("SendRequestTyped: %v", err)
	}
	if resp.GetStatus() != iop.NodeConfigRefreshStatus_NODE_CONFIG_REFRESH_STATUS_APPLIED {
		t.Fatalf("expected status=applied, got %v", resp.GetStatus())
	}
	if resp.GetRequestId() != "req-1" {
		t.Fatalf("expected request_id=req-1, got %q", resp.GetRequestId())
	}
}

// TestSessionConfigRefreshRequestHandlerErrorReturnsFailure verifies that a
// handler error is translated to a failed protocol response.
func TestSessionConfigRefreshRequestHandlerErrorReturnsFailure(t *testing.T) {
	edgeSide, nodeSide := buildSessionTestPipe(t)

	sess := transport.ExportNewSession(nodeSide, zap.NewNop(), "node-test", "alias-test")
	sess.SetHandler(&errorHandler{})

	resp, err := toki.SendRequestTyped[*iop.NodeConfigRefreshRequest, *iop.NodeConfigRefreshResponse](
		&edgeSide.Communicator,
		&iop.NodeConfigRefreshRequest{RequestId: "req-err"},
		2*time.Second,
	)
	if err != nil {
		t.Fatalf("SendRequestTyped: %v", err)
	}
	if resp.GetStatus() != iop.NodeConfigRefreshStatus_NODE_CONFIG_REFRESH_STATUS_FAILED {
		t.Fatalf("expected status=failed, got %v", resp.GetStatus())
	}
	if resp.GetError() == "" {
		t.Fatal("expected non-empty error message")
	}
}

// TestSessionConfigRefreshRequestNoHandlerReturnsFailure verifies that when no
// handler is set, the session returns a failed response with an informative message.
func TestSessionConfigRefreshRequestNoHandlerReturnsFailure(t *testing.T) {
	edgeSide, nodeSide := buildSessionTestPipe(t)

	_ = transport.ExportNewSession(nodeSide, zap.NewNop(), "node-nohandler", "")
	// handler intentionally NOT set

	resp, err := toki.SendRequestTyped[*iop.NodeConfigRefreshRequest, *iop.NodeConfigRefreshResponse](
		&edgeSide.Communicator,
		&iop.NodeConfigRefreshRequest{RequestId: "req-nohandler"},
		2*time.Second,
	)
	if err != nil {
		t.Fatalf("SendRequestTyped: %v", err)
	}
	if resp.GetStatus() != iop.NodeConfigRefreshStatus_NODE_CONFIG_REFRESH_STATUS_FAILED {
		t.Fatalf("expected status=failed when handler is nil, got %v", resp.GetStatus())
	}
}

type tunnelHandler struct {
	noopHandler
	mu       sync.Mutex
	requests []*iop.ProviderTunnelRequest
	done     chan struct{}
}

func (h *tunnelHandler) OnProviderTunnelRequest(_ context.Context, _ *transport.Session, req *iop.ProviderTunnelRequest) error {
	h.mu.Lock()
	h.requests = append(h.requests, req)
	h.mu.Unlock()
	close(h.done)
	return nil
}

func TestSessionProviderTunnelRequest(t *testing.T) {
	edgeSide, nodeSide := buildSessionTestPipe(t)

	sess := transport.ExportNewSession(nodeSide, zap.NewNop(), "node-test", "alias-test")
	handler := &tunnelHandler{done: make(chan struct{})}
	sess.SetHandler(handler)

	req := &iop.ProviderTunnelRequest{
		RunId:    "run-1",
		TunnelId: "tunnel-1",
		Adapter:  "openai_compat",
		Target:   "target-1",
	}

	if err := edgeSide.Send(req); err != nil {
		t.Fatalf("Send: %v", err)
	}

	select {
	case <-handler.done:
	case <-time.After(2 * time.Second):
		t.Fatal("timeout waiting for tunnel request")
	}

	handler.mu.Lock()
	defer handler.mu.Unlock()
	if len(handler.requests) != 1 {
		t.Fatalf("expected 1 request, got %d", len(handler.requests))
	}
	got := handler.requests[0]
	if got.GetRunId() != "run-1" || got.GetTunnelId() != "tunnel-1" {
		t.Errorf("unexpected request fields: %+v", got)
	}
}

// Compile check: Session must export a way to create instances for tests.
// ExportNewSession is expected in session_export_test.go or a separate test helper file.
var _ = fmt.Sprintf

type errorTunnelHandler struct {
	noopHandler
	done chan struct{}
}

func (h *errorTunnelHandler) OnProviderTunnelRequest(_ context.Context, _ *transport.Session, req *iop.ProviderTunnelRequest) error {
	defer close(h.done)
	return errors.New("tunnel error")
}

func TestSessionProviderTunnelRequest_NilAndErrHandler(t *testing.T) {
	// 1. Nil handler test
	{
		edgeSide, nodeSide := buildSessionTestPipe(t)
		sess := transport.ExportNewSession(nodeSide, zap.NewNop(), "node-test-nil", "alias-test")
		// handler is nil

		req := &iop.ProviderTunnelRequest{
			RunId:    "run-nil",
			TunnelId: "tunnel-nil",
			Adapter:  "openai_compat",
			Target:   "target-nil",
		}

		if err := edgeSide.Send(req); err != nil {
			t.Fatalf("Send: %v", err)
		}

		// wait a bit to ensure no panic
		time.Sleep(100 * time.Millisecond)
		sess.Close()
	}

	// 2. Error handler test
	{
		edgeSide, nodeSide := buildSessionTestPipe(t)
		sess := transport.ExportNewSession(nodeSide, zap.NewNop(), "node-test-err", "alias-test")
		handler := &errorTunnelHandler{done: make(chan struct{})}
		sess.SetHandler(handler)

		req := &iop.ProviderTunnelRequest{
			RunId:    "run-err",
			TunnelId: "tunnel-err",
			Adapter:  "openai_compat",
			Target:   "target-err",
		}

		if err := edgeSide.Send(req); err != nil {
			t.Fatalf("Send: %v", err)
		}

		select {
		case <-handler.done:
		case <-time.After(2 * time.Second):
			t.Fatal("timeout waiting for tunnel request")
		}
		sess.Close()
	}
}

// TestSessionSignalReady verifies the node→edge dispatch-ready handshake:
// SignalReady sends a NodeReadyRequest carrying the session's node id and returns
// nil only when the edge acks ready, and an error when the edge rejects the
// signal (stale connection) so the caller can tear down and reconnect.
func TestSessionSignalReady(t *testing.T) {
	newReadyPipe := func(t *testing.T, ready bool, reason string) *transport.Session {
		t.Helper()
		edgeConn, nodeConn := net.Pipe()
		edgeParser := toki.ParserMap{
			toki.TypeNameOf(&iop.NodeReadyRequest{}): func(b []byte) (proto.Message, error) {
				m := &iop.NodeReadyRequest{}
				return m, proto.Unmarshal(b, m)
			},
		}
		nodeParser := toki.ParserMap{
			toki.TypeNameOf(&iop.NodeReadyResponse{}): func(b []byte) (proto.Message, error) {
				m := &iop.NodeReadyResponse{}
				return m, proto.Unmarshal(b, m)
			},
		}
		edgeSide := toki.NewTcpClient(edgeConn, 0, 0, edgeParser)
		nodeSide := toki.NewTcpClient(nodeConn, 0, 0, nodeParser)
		t.Cleanup(func() { edgeSide.Close(); nodeSide.Close() })

		toki.AddRequestListenerTyped[*iop.NodeReadyRequest, *iop.NodeReadyResponse](
			&edgeSide.Communicator,
			func(req *iop.NodeReadyRequest) (*iop.NodeReadyResponse, error) {
				if req.GetNodeId() != "node-ready-test" {
					return &iop.NodeReadyResponse{Ready: false, Reason: "unexpected node id"}, nil
				}
				return &iop.NodeReadyResponse{Ready: ready, Reason: reason}, nil
			},
		)
		return transport.ExportNewSession(nodeSide, zap.NewNop(), "node-ready-test", "alias")
	}

	t.Run("ready ack succeeds", func(t *testing.T) {
		sess := newReadyPipe(t, true, "")
		if err := sess.SignalReady(2 * time.Second); err != nil {
			t.Fatalf("SignalReady on ready ack: %v", err)
		}
	})

	t.Run("non-ready ack errors", func(t *testing.T) {
		sess := newReadyPipe(t, false, "superseded")
		err := sess.SignalReady(2 * time.Second)
		if err == nil {
			t.Fatal("SignalReady must error when edge rejects the ready signal")
		}
	})
}
