package main

import (
	"context"
	"log/slog"
	"sync"
	"testing"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/protobuf/types/known/timestamppb"

	panelv1 "github.com/dbledeez/panel/proto/panel/v1"
)

// fakeAgentStream captures envelopes the controller sends to an agent.
// Only Send is exercised by these tests; the embedded nil ServerStream
// panics loudly if anything else is called.
type fakeAgentStream struct {
	grpc.ServerStream
	mu   sync.Mutex
	sent []*panelv1.ControllerEnvelope
}

func (f *fakeAgentStream) Send(env *panelv1.ControllerEnvelope) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.sent = append(f.sent, env)
	return nil
}

func (f *fakeAgentStream) Recv() (*panelv1.AgentEnvelope, error) {
	select {} // never called in these tests
}

func (f *fakeAgentStream) lastSent() *panelv1.ControllerEnvelope {
	f.mu.Lock()
	defer f.mu.Unlock()
	if len(f.sent) == 0 {
		return nil
	}
	return f.sent[len(f.sent)-1]
}

// TestSendUpdateCheckRoundTrip drives sendUpdateCheck against a fake
// agent: the request envelope goes out, a fake UpdateCheckResult comes
// back through the pending registry (as handleAgentMessage would deliver
// it), and the parsed result surfaces to the caller.
func TestSendUpdateCheckRoundTrip(t *testing.T) {
	stream := &fakeAgentStream{}
	conn := &agentConn{id: "agent-1", stream: stream}
	h := &httpServer{pending: newPendingRegistry(), log: slog.Default()}

	type outcome struct {
		res *panelv1.UpdateCheckResult
		err error
	}
	done := make(chan outcome, 1)
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		res, err := h.sendUpdateCheck(ctx, conn, "inst-7dtd", "v2_6", false)
		done <- outcome{res, err}
	}()

	// Wait for the outbound envelope, then answer it like the agent would.
	var env *panelv1.ControllerEnvelope
	deadline := time.Now().Add(2 * time.Second)
	for env == nil && time.Now().Before(deadline) {
		env = stream.lastSent()
		if env == nil {
			time.Sleep(5 * time.Millisecond)
		}
	}
	if env == nil {
		t.Fatal("no envelope sent to agent")
	}
	req := env.GetUpdateCheck()
	if req == nil {
		t.Fatalf("sent envelope is not an UpdateCheck: %+v", env)
	}
	if req.InstanceId != "inst-7dtd" || req.ProviderId != "v2_6" {
		t.Fatalf("bad request: %+v", req)
	}

	reply := &panelv1.AgentEnvelope{
		CorrelationId: env.CorrelationId,
		SentAt:        timestamppb.Now(),
		Payload: &panelv1.AgentEnvelope_UpdateCheckResult{UpdateCheckResult: &panelv1.UpdateCheckResult{
			InstanceId:       "inst-7dtd",
			AppId:            "294420",
			Branch:           "v2.6",
			InstalledBuildid: "14503775",
			LatestBuildid:    "14600000",
			UpdateAvailable:  true,
		}},
	}
	// Route through handleAgentMessage — this is the regression guard for
	// the "wired into the correlation switch" golden rule: if the case is
	// missing, delivery never happens and sendUpdateCheck times out.
	srv := &agentServer{log: slog.Default(), pending: h.pending}
	srv.handleAgentMessage(context.Background(), &agentConn{id: "agent-1"}, reply)

	o := <-done
	if o.err != nil {
		t.Fatalf("sendUpdateCheck: %v", o.err)
	}
	if !o.res.UpdateAvailable || o.res.InstalledBuildid != "14503775" || o.res.LatestBuildid != "14600000" {
		t.Fatalf("bad result: %+v", o.res)
	}
}

// TestSendUpdateCheckWrongPayload ensures a mis-typed reply surfaces as
// an error instead of a nil-deref.
func TestSendUpdateCheckWrongPayload(t *testing.T) {
	stream := &fakeAgentStream{}
	conn := &agentConn{id: "agent-1", stream: stream}
	h := &httpServer{pending: newPendingRegistry(), log: slog.Default()}

	errCh := make(chan error, 1)
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		_, err := h.sendUpdateCheck(ctx, conn, "inst-x", "", true)
		errCh <- err
	}()
	var env *panelv1.ControllerEnvelope
	deadline := time.Now().Add(2 * time.Second)
	for env == nil && time.Now().Before(deadline) {
		env = stream.lastSent()
		if env == nil {
			time.Sleep(5 * time.Millisecond)
		}
	}
	if env == nil {
		t.Fatal("no envelope sent to agent")
	}
	if got := env.GetUpdateCheck(); got == nil || !got.Refresh {
		t.Fatalf("expected refresh=true UpdateCheck, got %+v", env)
	}
	h.pending.Deliver(env.CorrelationId, &panelv1.AgentEnvelope{
		CorrelationId: env.CorrelationId,
		Payload:       &panelv1.AgentEnvelope_Pong{Pong: &panelv1.Pong{}},
	})
	if err := <-errCh; err == nil {
		t.Fatal("expected error for wrong payload type")
	}
}
