package servertest

import (
	"fmt"
	"testing"
	"time"

	"github.com/juanfont/headscale/hscontrol/types"
)

// TestHarness orchestrates a TestServer with multiple TestClients,
// providing a convenient setup for multi-node control plane tests.
type TestHarness struct {
	Server  *TestServer
	clients []*TestClient

	// Default user shared by all clients unless overridden.
	defaultUser *types.User
}

// HarnessOption configures a TestHarness.
type HarnessOption func(*harnessConfig)

type harnessConfig struct {
	serverOpts     []ServerOption
	clientOpts     []ClientOption
	convergenceMax time.Duration
}

func defaultHarnessConfig() *harnessConfig {
	return &harnessConfig{
		convergenceMax: 30 * time.Second,
	}
}

// WithServerOptions passes ServerOptions through to the underlying
// TestServer.
func WithServerOptions(opts ...ServerOption) HarnessOption {
	return func(c *harnessConfig) { c.serverOpts = append(c.serverOpts, opts...) }
}

// WithDefaultClientOptions applies ClientOptions to every client
// created by NewHarness.
func WithDefaultClientOptions(opts ...ClientOption) HarnessOption {
	return func(c *harnessConfig) { c.clientOpts = append(c.clientOpts, opts...) }
}

// WithConvergenceTimeout sets how long WaitForMeshComplete waits.
func WithConvergenceTimeout(d time.Duration) HarnessOption {
	return func(c *harnessConfig) { c.convergenceMax = d }
}

// NewHarness creates a TestServer and numClients connected clients.
// All clients share a default user and are registered with reusable
// pre-auth keys. The harness waits for all clients to form a
// complete mesh before returning.
func NewHarness(tb testing.TB, numClients int, opts ...HarnessOption) *TestHarness {
	tb.Helper()

	hc := defaultHarnessConfig()
	for _, o := range opts {
		o(hc)
	}

	server := NewServer(tb, hc.serverOpts...)

	// Create a shared default user.
	user := server.CreateUser(tb, "harness-default")

	h := &TestHarness{
		Server:      server,
		defaultUser: user,
	}

	// Create and connect clients.
	for i := range numClients {
		name := clientName(i)

		copts := append([]ClientOption{WithUser(user)}, hc.clientOpts...)
		c := NewClient(tb, server, name, copts...)
		h.clients = append(h.clients, c)
	}

	// Wait for the mesh to converge.
	if numClients > 1 {
		h.WaitForMeshComplete(tb, hc.convergenceMax)
	} else if numClients == 1 {
		// Single node: just wait for the first netmap.
		h.clients[0].WaitForUpdate(tb, hc.convergenceMax)
	}

	return h
}

// Client returns the i-th client (0-indexed).
func (h *TestHarness) Client(i int) *TestClient {
	return h.clients[i]
}

// Clients returns all clients.
func (h *TestHarness) Clients() []*TestClient {
	return h.clients
}

// ConnectedClients returns clients that currently have an active
// long-poll session (pollDone channel is still open).
func (h *TestHarness) ConnectedClients() []*TestClient {
	var out []*TestClient

	for _, c := range h.clients {
		select {
		case <-c.pollDone:
			// Poll has ended, client is disconnected.
		default:
			out = append(out, c)
		}
	}

	return out
}

// AddClient creates and connects a new client to the existing mesh.
func (h *TestHarness) AddClient(tb testing.TB, opts ...ClientOption) *TestClient {
	tb.Helper()

	name := clientName(len(h.clients))
	copts := append([]ClientOption{WithUser(h.defaultUser)}, opts...)
	c := NewClient(tb, h.Server, name, copts...)
	h.clients = append(h.clients, c)

	return c
}

// WaitForMeshComplete blocks until every connected client sees
// (connectedCount - 1) peers.
func (h *TestHarness) WaitForMeshComplete(tb testing.TB, timeout time.Duration) {
	tb.Helper()

	connected := h.ConnectedClients()

	expectedPeers := max(len(connected)-1, 0)

	for _, c := range connected {
		c.WaitForPeers(tb, expectedPeers, timeout)
	}
}

// WaitForConvergence waits until all connected clients have a
// non-nil NetworkMap and their peer counts have stabilised.
func (h *TestHarness) WaitForConvergence(tb testing.TB, timeout time.Duration) {
	tb.Helper()
	h.WaitForMeshComplete(tb, timeout)
}

// ChangePolicy sets an ACL policy on the server and propagates changes
// to all connected nodes. The policy should be a valid HuJSON policy document.
func (h *TestHarness) ChangePolicy(tb testing.TB, policy []byte) {
	tb.Helper()

	changed, err := h.Server.State().SetPolicy(policy)
	if err != nil {
		tb.Fatalf("servertest: ChangePolicy: %v", err)
	}

	if changed {
		changes, err := h.Server.State().ReloadPolicy()
		if err != nil {
			tb.Fatalf("servertest: ReloadPolicy: %v", err)
		}

		h.Server.App.Change(changes...)
	}
}

// DefaultUser returns the shared user for adding more clients.
func (h *TestHarness) DefaultUser() *types.User {
	return h.defaultUser
}

func clientName(index int) string {
	return fmt.Sprintf("node-%d", index)
}
