package agent

import (
	"database/sql"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"testing"

	"git.wntrmute.dev/mc/mcp/internal/registry"
)

func openTestDB(t *testing.T) *sql.DB {
	t.Helper()
	db, err := registry.Open(filepath.Join(t.TempDir(), "test.db"))
	if err != nil {
		t.Fatalf("open db: %v", err)
	}
	t.Cleanup(func() { _ = db.Close() })
	return db
}

func testAgent(t *testing.T) *Agent {
	t.Helper()
	return &Agent{
		DB:        openTestDB(t),
		PortAlloc: NewPortAllocator(),
		Logger:    slog.New(slog.NewTextHandler(os.Stderr, nil)),
	}
}

// seedComponent creates the service and component in the registry so that
// allocateRoutePorts can store host ports for it.
func seedComponent(t *testing.T, db *sql.DB, service, component string, routes []registry.Route) {
	t.Helper()
	if err := registry.CreateService(db, service, true); err != nil {
		t.Fatalf("create service: %v", err)
	}
	if err := registry.CreateComponent(db, &registry.Component{
		Name:          component,
		Service:       service,
		Image:         "img:latest",
		DesiredState:  "running",
		ObservedState: "unknown",
		Routes:        routes,
	}); err != nil {
		t.Fatalf("create component: %v", err)
	}
}

func TestAllocateRoutePorts_SingleRoute(t *testing.T) {
	a := testAgent(t)
	routes := []registry.Route{
		{Name: "default", Port: 443, Mode: "l7"},
	}
	seedComponent(t, a.DB, "mcdoc", "mcdoc", routes)

	ports, env, err := a.allocateRoutePorts("mcdoc", "mcdoc", routes)
	if err != nil {
		t.Fatalf("allocateRoutePorts: %v", err)
	}

	if len(ports) != 1 {
		t.Fatalf("expected 1 port mapping, got %d", len(ports))
	}
	if len(env) != 1 {
		t.Fatalf("expected 1 env var, got %d", len(env))
	}

	// Parse the port mapping: should be "127.0.0.1:<hostPort>:<hostPort>"
	// NOT "127.0.0.1:<hostPort>:443"
	var hostPort, containerPort int
	n, _ := fmt.Sscanf(ports[0], "127.0.0.1:%d:%d", &hostPort, &containerPort)
	if n != 2 {
		t.Fatalf("failed to parse port mapping %q", ports[0])
	}
	if hostPort != containerPort {
		t.Errorf("host port (%d) != container port (%d); container port must match host port for $PORT consistency", hostPort, containerPort)
	}

	// Env var should be PORT=<hostPort>
	var envPort int
	n, _ = fmt.Sscanf(env[0], "PORT=%d", &envPort)
	if n != 1 {
		t.Fatalf("failed to parse env var %q", env[0])
	}
	if envPort != hostPort {
		t.Errorf("PORT env (%d) != host port (%d)", envPort, hostPort)
	}
}

func TestAllocateRoutePorts_MultiRoute(t *testing.T) {
	a := testAgent(t)
	routes := []registry.Route{
		{Name: "rest", Port: 8443, Mode: "l4"},
		{Name: "grpc", Port: 9443, Mode: "l4"},
	}
	seedComponent(t, a.DB, "metacrypt", "api", routes)

	ports, env, err := a.allocateRoutePorts("metacrypt", "api", routes)
	if err != nil {
		t.Fatalf("allocateRoutePorts: %v", err)
	}

	if len(ports) != 2 {
		t.Fatalf("expected 2 port mappings, got %d", len(ports))
	}
	if len(env) != 2 {
		t.Fatalf("expected 2 env vars, got %d", len(env))
	}

	// Each port mapping should have host port == container port.
	for i, p := range ports {
		var hp, cp int
		n, _ := fmt.Sscanf(p, "127.0.0.1:%d:%d", &hp, &cp)
		if n != 2 {
			t.Fatalf("port[%d]: failed to parse %q", i, p)
		}
		if hp != cp {
			t.Errorf("port[%d]: host port (%d) != container port (%d)", i, hp, cp)
		}
	}

	// Env vars should be PORT_REST and PORT_GRPC (not bare PORT).
	if env[0][:10] != "PORT_REST=" {
		t.Errorf("env[0] = %q, want PORT_REST=...", env[0])
	}
	if env[1][:10] != "PORT_GRPC=" {
		t.Errorf("env[1] = %q, want PORT_GRPC=...", env[1])
	}
}

func TestAllocateRoutePorts_L7PortNotUsedAsContainerPort(t *testing.T) {
	a := testAgent(t)
	routes := []registry.Route{
		{Name: "default", Port: 443, Mode: "l7"},
	}
	seedComponent(t, a.DB, "svc", "web", routes)

	ports, _, err := a.allocateRoutePorts("svc", "web", routes)
	if err != nil {
		t.Fatalf("allocateRoutePorts: %v", err)
	}

	// The container port must NOT be 443 (the mc-proxy listener port).
	// It must be the host port (which is in range 10000-60000).
	var hostPort, containerPort int
	n, _ := fmt.Sscanf(ports[0], "127.0.0.1:%d:%d", &hostPort, &containerPort)
	if n != 2 {
		t.Fatalf("failed to parse port mapping %q", ports[0])
	}
	if containerPort == 443 {
		t.Errorf("container port is 443 (mc-proxy listener); should be %d (host port)", hostPort)
	}
	if containerPort < portRangeMin || containerPort >= portRangeMax {
		t.Errorf("container port %d outside allocation range [%d, %d)", containerPort, portRangeMin, portRangeMax)
	}
}
