package server

import (
	"context"
	"database/sql"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"testing"

	"github.com/rs/zerolog"
	_ "modernc.org/sqlite"

	"github.com/operator/command-center/internal/config"
	"github.com/operator/command-center/internal/observability"
)

// minimalSchemaSQL is the slice of the Phase 0 SQLite schema this test needs.
// Kept inline so the server package test does not depend on the embed package
// (which would create a circular dependency via `commandcenter`).
const minimalSchemaSQL = `
CREATE TABLE system_events (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    timestamp INTEGER NOT NULL,
    level TEXT NOT NULL,
    component TEXT NOT NULL,
    message TEXT NOT NULL,
    context_json TEXT,
    correlation_id TEXT
);`

func newTestServer(t *testing.T) *Server {
	t.Helper()
	dbPath := filepath.Join(t.TempDir(), "test.db")
	db, err := sql.Open("sqlite", dbPath)
	if err != nil {
		t.Fatalf("open sqlite: %v", err)
	}
	t.Cleanup(func() { _ = db.Close() })
	if _, err := db.Exec(minimalSchemaSQL); err != nil {
		t.Fatalf("create schema: %v", err)
	}

	cfg := &config.SystemConfig{
		DevMode: true,
		Listen: config.ListenConfig{
			Port:                      8443,
			TailscaleInterfacePattern: "this-name-does-not-exist*",
			ReadTimeoutSeconds:        5,
			WriteTimeoutSeconds:       5,
		},
	}
	logger := zerolog.Nop()
	events := observability.NewEventRecorder(db, logger)
	events.Record(context.Background(), observability.LevelInfo, "boot", "test boot", nil, "")
	health := observability.NewHealthChecker(db, nil, "/tmp/age.key", "test", false)
	health.MarkConfigLoaded()

	srv, err := New(Options{
		Config: cfg,
		Logger: logger,
		Health: health,
		Events: events,
	})
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	return srv
}

func TestHealthHandler(t *testing.T) {
	srv := newTestServer(t)
	h := srv.buildHandler()

	rr := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, "/api/system/health", nil)
	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusOK {
		t.Fatalf("status: got %d, want 200", rr.Code)
	}
	var body observability.HealthStatus
	if err := json.NewDecoder(rr.Body).Decode(&body); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if body.Version != "test" {
		t.Errorf("version: got %q, want test", body.Version)
	}
	if len(body.Checks) != 4 {
		t.Errorf("checks: got %d, want 4", len(body.Checks))
	}
	// DuckDB is degraded (not compiled in for this test), overall should be degraded.
	if body.Status != observability.StatusDegraded {
		t.Errorf("status: got %q, want degraded", body.Status)
	}
}

func TestEventsHandler(t *testing.T) {
	srv := newTestServer(t)
	h := srv.buildHandler()

	rr := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, "/api/system/events", nil)
	h.ServeHTTP(rr, req)

	if rr.Code != http.StatusOK {
		t.Fatalf("status: got %d, want 200", rr.Code)
	}
	body, _ := io.ReadAll(rr.Body)
	var parsed struct {
		Count  int                  `json:"count"`
		Events []observability.Event `json:"events"`
	}
	if err := json.Unmarshal(body, &parsed); err != nil {
		t.Fatalf("decode: %v\nbody=%s", err, string(body))
	}
	if parsed.Count != 1 {
		t.Errorf("count: got %d, want 1\nbody=%s", parsed.Count, string(body))
	}
	if len(parsed.Events) != 1 || parsed.Events[0].Component != "boot" {
		t.Errorf("events: %+v", parsed.Events)
	}

	// Filter by component that doesn't exist.
	rr = httptest.NewRecorder()
	req = httptest.NewRequest(http.MethodGet, "/api/system/events?component=nonexistent", nil)
	h.ServeHTTP(rr, req)
	body, _ = io.ReadAll(rr.Body)
	if !strings.Contains(string(body), `"count":0`) {
		t.Errorf("filtered count not zero: %s", string(body))
	}
}

func TestResolveBindAddrFailsWithoutDevMode(t *testing.T) {
	cfg := &config.SystemConfig{
		DevMode: false,
		Listen: config.ListenConfig{
			Port:                      8443,
			TailscaleInterfacePattern: "this-name-does-not-exist*",
		},
	}
	_, err := resolveBindAddr(cfg, zerolog.Nop())
	if err == nil {
		t.Fatal("expected error when Tailscale interface is absent and dev_mode is off")
	}
	if !strings.Contains(err.Error(), "tailscale") {
		t.Errorf("error mention tailscale: got %q", err.Error())
	}
}

func TestResolveBindAddrDevModeFallsBack(t *testing.T) {
	cfg := &config.SystemConfig{
		DevMode: true,
		Listen: config.ListenConfig{
			Port:                      8443,
			TailscaleInterfacePattern: "this-name-does-not-exist*",
		},
	}
	addr, err := resolveBindAddr(cfg, zerolog.Nop())
	if err != nil {
		t.Fatalf("dev_mode resolveBindAddr: %v", err)
	}
	if !strings.HasPrefix(addr, "127.0.0.1:") {
		t.Errorf("dev_mode addr: got %q, want 127.0.0.1:*", addr)
	}
}
