// tree_rpc_test.go — full round-trip test for the tree.list synchronous
// RPC (P3-X2). A fake agent reads the inbound tree.list, replies with a
// canned tree.list.result, and we assert the server's SendRPC returned
// the expected payload.
package http

import (
	"context"
	"encoding/json"
	"testing"
	"time"

	"github.com/coder/websocket"

	"gitea.dcglab.co.uk/steve/restic-manager/internal/api"
)

func TestSendRPCTreeListRoundTrip(t *testing.T) {
	t.Parallel()
	srv, ts, st := rawTestServer(t)
	hostID, token := enrolHostForWS(t, srv, st, "rpc-host")
	c := agentDial(t, srv, ts, hostID, token)
	sendHello(t, c, "rpc-host")
	_ = drainUntil(t, c, api.MsgScheduleSet)

	// Fake agent: read inbound envelopes, mirror tree.list with a
	// canned result. Other inbound envelopes (config.update etc) are
	// already drained above.
	done := make(chan error, 1)
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		for {
			mt, raw, err := c.Read(ctx)
			if err != nil {
				done <- err
				return
			}
			if mt != websocket.MessageText {
				continue
			}
			var env api.Envelope
			if err := json.Unmarshal(raw, &env); err != nil {
				done <- err
				return
			}
			if env.Type != api.MsgTreeList {
				continue
			}
			var req api.TreeListRequestPayload
			if err := env.UnmarshalPayload(&req); err != nil {
				done <- err
				return
			}
			result := api.TreeListResultPayload{
				SnapshotID: req.SnapshotID,
				Path:       req.Path,
				Entries: []api.TreeListEntry{
					{Name: "etc", Type: "dir"},
					{Name: "var", Type: "dir"},
				},
			}
			out, err := api.Marshal(api.MsgTreeListResult, env.ID, result)
			if err != nil {
				done <- err
				return
			}
			rawOut, _ := json.Marshal(out)
			if err := c.Write(ctx, websocket.MessageText, rawOut); err != nil {
				done <- err
				return
			}
			done <- nil
			return
		}
	}()

	// Server-side SendRPC.
	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
	defer cancel()
	reply, err := srv.deps.Hub.SendRPC(ctx, hostID, api.MsgTreeList,
		api.TreeListRequestPayload{SnapshotID: "f3a7b2c1", Path: "/"},
		3*time.Second)
	if err != nil {
		t.Fatalf("SendRPC: %v", err)
	}
	if reply.Type != api.MsgTreeListResult {
		t.Fatalf("reply type: got %q want %q", reply.Type, api.MsgTreeListResult)
	}
	var result api.TreeListResultPayload
	if err := reply.UnmarshalPayload(&result); err != nil {
		t.Fatalf("unmarshal reply: %v", err)
	}
	if result.SnapshotID != "f3a7b2c1" || result.Path != "/" {
		t.Fatalf("payload: got %+v", result)
	}
	if len(result.Entries) != 2 || result.Entries[0].Name != "etc" {
		t.Fatalf("entries: %+v", result.Entries)
	}

	// Make sure the fake agent didn't error out.
	select {
	case err := <-done:
		if err != nil {
			t.Fatalf("fake agent: %v", err)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("fake agent didn't finish")
	}
}

// TestSendRPCTimeoutNoReply: SendRPC times out cleanly when the agent
// never replies; the registry entry is released so a stray late reply
// wouldn't deadlock anything.
func TestSendRPCTimeoutNoReply(t *testing.T) {
	t.Parallel()
	srv, ts, st := rawTestServer(t)
	hostID, token := enrolHostForWS(t, srv, st, "rpc-timeout-host")
	c := agentDial(t, srv, ts, hostID, token)
	sendHello(t, c, "rpc-timeout-host")
	_ = drainUntil(t, c, api.MsgScheduleSet)

	// Fake agent reads but never replies.
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
		defer cancel()
		for {
			if _, _, err := c.Read(ctx); err != nil {
				return
			}
		}
	}()

	ctx := context.Background()
	t0 := time.Now()
	_, err := srv.deps.Hub.SendRPC(ctx, hostID, api.MsgTreeList,
		api.TreeListRequestPayload{SnapshotID: "x", Path: "/"},
		300*time.Millisecond)
	if err == nil {
		t.Fatal("expected timeout error")
	}
	elapsed := time.Since(t0)
	if elapsed < 250*time.Millisecond || elapsed > 2*time.Second {
		t.Fatalf("timeout took %s, expected ~300ms", elapsed)
	}
}
