package api

import (
	"bytes"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"crypto-miner-server/internal/builder"
	"crypto-miner-server/internal/db"
	"crypto-miner-server/internal/models"
	"crypto-miner-server/internal/pool"

	"github.com/gorilla/websocket"
)

type mockConfigProvider struct {
	raw json.RawMessage
}

func (m *mockConfigProvider) GetConfigJSON() json.RawMessage {
	if len(m.raw) == 0 {
		return json.RawMessage(`{"port":8989}`)
	}
	return m.raw
}

func (m *mockConfigProvider) UpdateConfigFromJSON(data json.RawMessage) error {
	m.raw = data
	return nil
}

const testAuthUser = "testuser"
const testAuthPass = "testpass"
const testFleetSecret = "test-fleet-secret-integration"

func seedTestUsers(t *testing.T, dataDir string) {
	t.Helper()
	usersPath := filepath.Join(dataDir, "users.json")
	data, err := json.Marshal(map[string]string{testAuthUser: testAuthPass})
	if err != nil {
		t.Fatalf("marshal users: %v", err)
	}
	if err := os.WriteFile(usersPath, data, 0600); err != nil {
		t.Fatalf("write users.json: %v", err)
	}
}

func newTestRouter(t *testing.T) (http.Handler, *WSHub, *db.Database, string) {
	t.Helper()
	dataDir := t.TempDir()
	seedTestUsers(t, dataDir)

	database, err := db.New(dataDir)
	if err != nil {
		t.Fatalf("db: %v", err)
	}
	t.Cleanup(func() { database.Close() })

	wsHub := NewWSHub(database)
	cfg := &mockConfigProvider{}
	configHandler := NewConfigHandler(cfg)
	aiHandler := NewAIHandler(database)
	fleetHandler := NewFleetHandler(database, wsHub, aiHandler, nil, nil, pool.Config{})
	builderHandler := builder.NewHandler(database, dataDir, "", dataDir)
	blueprintHandler := NewBlueprintHandler(dataDir)

	webRoot := filepath.Join(dataDir, "webroot")
	_ = os.MkdirAll(webRoot, 0755)
	_ = os.WriteFile(filepath.Join(webRoot, "index.html"), []byte("<html><body>AetherForge</body></html>"), 0644)

	dropperHandler := NewDropperHandler(database, nil)
	return NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, dropperHandler, nil, webRoot, dataDir, nil), wsHub, database, dataDir
}

func serveAuthed(t *testing.T, router http.Handler, method, path string, body []byte) *httptest.ResponseRecorder {
	t.Helper()
	var req *http.Request
	if body != nil {
		req = httptest.NewRequest(method, path, bytes.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
	} else {
		req = httptest.NewRequest(method, path, nil)
	}
	req.SetBasicAuth(testAuthUser, testAuthPass)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	return rec
}

// serveWithFleetSecret sends a request with the fleet secret header (for /api/v1/agent/* routes).
func serveWithFleetSecret(t *testing.T, router http.Handler, method, path, secret string, body []byte) *httptest.ResponseRecorder {
	t.Helper()
	var req *http.Request
	if body != nil {
		req = httptest.NewRequest(method, path, bytes.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
	} else {
		req = httptest.NewRequest(method, path, nil)
	}
	req.Header.Set("X-Fleet-Secret", secret)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	return rec
}

func insertTestBuild(t *testing.T, database *db.Database, dataDir, buildID, platform, fileName string) {
	t.Helper()
	buildDir := filepath.Join(dataDir, "builds", buildID)
	if err := os.MkdirAll(buildDir, 0755); err != nil {
		t.Fatal(err)
	}
	binPath := filepath.Join(buildDir, fileName)
	if err := os.WriteFile(binPath, []byte("fake-binary-"+platform), 0644); err != nil {
		t.Fatal(err)
	}
	if err := database.InsertBuild(&models.BuildRecord{
		ID: buildID, WorkerName: "worker", ServerURL: "http://localhost:8989", Wallet: "48x",
		FilePath: binPath, FileName: fileName, Platform: platform, CreatedAt: time.Now(),
	}); err != nil {
		t.Fatal(err)
	}
}

func startRouterServer(t *testing.T, router http.Handler) *httptest.Server {
	t.Helper()
	srv := httptest.NewServer(router)
	t.Cleanup(srv.Close)
	return srv
}

func connectAgentViaRouter(t *testing.T, router http.Handler, agentID string) (*websocket.Conn, *httptest.Server) {
	t.Helper()
	srv := startRouterServer(t, router)
	wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/ws/agent"
	conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
	if err != nil {
		t.Fatalf("dial agent ws: %v", err)
	}
	t.Cleanup(func() { _ = conn.Close() })

	authPayload, _ := json.Marshal(map[string]interface{}{
		"agent_id": agentID,
		"hostname": "integration-host",
		"version":  "1.0",
	})
	if err := conn.WriteJSON(Message{Type: "auth", Payload: authPayload}); err != nil {
		t.Fatalf("send auth: %v", err)
	}
	var resp Message
	if err := conn.ReadJSON(&resp); err != nil {
		t.Fatalf("read auth_response: %v", err)
	}
	if resp.Type != "auth_response" {
		t.Fatalf("expected auth_response, got %q", resp.Type)
	}
	return conn, srv
}

func TestHealthIsPublic(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	req := httptest.NewRequest(http.MethodGet, "/api/v1/health", nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("health status=%d body=%s", rec.Code, rec.Body.String())
	}
	var body map[string]string
	if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
		t.Fatal(err)
	}
	if body["status"] != "ok" {
		t.Fatalf("unexpected health: %v", body)
	}
}

func TestConfigRequiresAuth(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	req := httptest.NewRequest(http.MethodGet, "/api/v1/config", nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusUnauthorized {
		t.Fatalf("expected 401, got %d", rec.Code)
	}
}

func TestConfigWithValidAuth(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	req := httptest.NewRequest(http.MethodGet, "/api/v1/config", nil)
	req.SetBasicAuth(testAuthUser, testAuthPass)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
	}
}

func TestAgentsListRequiresAuth(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	req := httptest.NewRequest(http.MethodGet, "/api/v1/agents", nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusUnauthorized {
		t.Fatalf("expected 401, got %d", rec.Code)
	}
}

func TestAgentsListAuthedEmpty(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	req := httptest.NewRequest(http.MethodGet, "/api/v1/agents", nil)
	req.SetBasicAuth(testAuthUser, testAuthPass)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", rec.Code)
	}
	var agents []json.RawMessage
	if err := json.Unmarshal(rec.Body.Bytes(), &agents); err != nil {
		t.Fatal(err)
	}
	if len(agents) != 0 {
		t.Fatalf("expected empty fleet, got %d", len(agents))
	}
}

func TestArtifactDownloadRejectsTraversal(t *testing.T) {
	router, _, _, dataDir := newTestRouter(t)
	buildID := "test-build-id"
	buildDir := filepath.Join(dataDir, "builds", buildID)
	if err := os.MkdirAll(buildDir, 0755); err != nil {
		t.Fatal(err)
	}
	req := httptest.NewRequest(http.MethodGet, "/api/v1/builds/"+buildID+"/artifact/..%2F..%2Fsecret.txt", nil)
	req.SetBasicAuth(testAuthUser, testAuthPass)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusBadRequest && rec.Code != http.StatusNotFound {
		t.Fatalf("expected rejection, got %d body=%s", rec.Code, rec.Body.String())
	}
}

func TestSPAServesIndex(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	req := httptest.NewRequest(http.MethodGet, "/dashboard", nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", rec.Code)
	}
	if !contains(rec.Body.String(), "AetherForge") {
		t.Fatalf("expected SPA fallback html")
	}
}

func contains(s, sub string) bool {
	return len(s) >= len(sub) && (s == sub || len(sub) == 0 || indexOf(s, sub) >= 0)
}

func indexOf(s, sub string) int {
	for i := 0; i+len(sub) <= len(s); i++ {
		if s[i:i+len(sub)] == sub {
			return i
		}
	}
	return -1
}

func TestStatsLimitCapped(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	req := httptest.NewRequest(http.MethodGet, "/api/v1/agents/nope/stats?limit=999999", nil)
	req.SetBasicAuth(testAuthUser, testAuthPass)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	// Agent may not exist — 404 is fine; we only care handler doesn't 500 on huge limit
	if rec.Code == http.StatusInternalServerError {
		t.Fatalf("limit cap caused server error: %s", rec.Body.String())
	}
}

func TestIntegrationServerInfo(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	rec := serveAuthed(t, router, http.MethodGet, "/api/v1/server/info", nil)
	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
	var body map[string]interface{}
	if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
		t.Fatal(err)
	}
	if _, ok := body["port"]; !ok {
		t.Fatalf("expected port in server info: %v", body)
	}
}

func TestIntegrationDashboardStats(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	rec := serveAuthed(t, router, http.MethodGet, "/api/v1/dashboard/stats", nil)
	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
}

func TestIntegrationGetAgentNotFound(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	rec := serveAuthed(t, router, http.MethodGet, "/api/v1/agents/missing-agent", nil)
	if rec.Code != http.StatusNotFound {
		t.Fatalf("expected 404, got %d", rec.Code)
	}
}

func TestIntegrationAgentLog(t *testing.T) {
	router, wsHub, _, _ := newTestRouter(t)
	agentID := "log-agent"
	connectTestAgent(t, wsHub, agentID)

	rec := serveAuthed(t, router, http.MethodGet, "/api/v1/agents/"+agentID+"/log", nil)
	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
	var body map[string]interface{}
	if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
		t.Fatal(err)
	}
	if body["agent_id"] != agentID {
		t.Fatalf("unexpected agent_id: %v", body["agent_id"])
	}
}

func TestIntegrationAgentCommandOffline(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	rec := serveAuthed(t, router, http.MethodPost, "/api/v1/agents/offline-agent/command",
		[]byte(`{"action":"pause"}`))
	// Command for a non-connected agent returns 200 with success:false (not a 4xx),
	// so the caller can inspect the error without tripping HTTP error handling.
	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200 for offline agent, got %d body=%s", rec.Code, rec.Body.String())
	}
	var body map[string]interface{}
	if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
		t.Fatal(err)
	}
	if body["success"] != false {
		t.Fatalf("expected success=false, got %v", body)
	}
}

func TestIntegrationAgentMeta(t *testing.T) {
	router, _, database, _ := newTestRouter(t)
	agent := &models.Agent{ID: "meta-rig", Name: "rig", Status: "offline", LastSeen: time.Now()}
	if err := database.UpsertAgent(agent); err != nil {
		t.Fatal(err)
	}

	rec := serveAuthed(t, router, http.MethodPut, "/api/v1/agents/meta-rig/meta",
		[]byte(`{"notes":"integration test","tags":["lab"]}`))
	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
}

func TestIntegrationBulkCommandPartialFailure(t *testing.T) {
	router, wsHub, _, _ := newTestRouter(t)
	onlineID := "bulk-online"
	connectTestAgent(t, wsHub, onlineID)

	payload := `{"agent_ids":["` + onlineID + `","offline-one"],"action":"pause","command":"tasklist"}`
	rec := serveAuthed(t, router, http.MethodPost, "/api/v1/agents/bulk-command", []byte(payload))
	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
	var body map[string]interface{}
	if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
		t.Fatal(err)
	}
	if body["success"] != true {
		t.Fatalf("expected partial success true, got %v", body)
	}
	if body["sent"].(float64) != 1 || body["failed"].(float64) != 1 {
		t.Fatalf("sent/failed counts: %v", body)
	}
}

func TestIntegrationFleetReadEndpoints(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	paths := []string{
		"/api/v1/alerts",
		"/api/v1/pools/status",
		"/api/v1/ai/activity",
		"/api/v1/earnings/estimate",
		"/api/v1/shares",
		"/api/v1/builds",
	}
	for _, path := range paths {
		rec := serveAuthed(t, router, http.MethodGet, path, nil)
		if rec.Code != http.StatusOK {
			t.Fatalf("%s status=%d body=%s", path, rec.Code, rec.Body.String())
		}
	}
}

func TestIntegrationMarketXMR(t *testing.T) {
	setMockHTTPTransport(t, roundTripFunc(func(req *http.Request) (*http.Response, error) {
		if !strings.Contains(req.URL.String(), "coingecko") {
			return nil, errors.New("unexpected url")
		}
		body := `{"monero":{"usd":165.5}}`
		return &http.Response{
			StatusCode: http.StatusOK,
			Body:       io.NopCloser(strings.NewReader(body)),
			Header:     http.Header{"Content-Type": []string{"application/json"}},
		}, nil
	}))

	router, _, _, _ := newTestRouter(t)
	rec := serveAuthed(t, router, http.MethodGet, "/api/v1/market/xmr", nil)
	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
	var body map[string]interface{}
	if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
		t.Fatal(err)
	}
	if body["usd"].(float64) != 165.5 {
		t.Fatalf("unexpected price: %v", body["usd"])
	}
}

func TestIntegrationBuildsLifecycle(t *testing.T) {
	router, _, database, dataDir := newTestRouter(t)
	buildID := "lifecycle-build"
	insertTestBuild(t, database, dataDir, buildID, "windows", "worker.exe")

	rec := serveAuthed(t, router, http.MethodGet, "/api/v1/builds", nil)
	if rec.Code != http.StatusOK {
		t.Fatalf("list status=%d", rec.Code)
	}

	rec = serveAuthed(t, router, http.MethodPut, "/api/v1/builds/"+buildID+"/pin", nil)
	if rec.Code != http.StatusOK {
		t.Fatalf("pin status=%d body=%s", rec.Code, rec.Body.String())
	}

	rec = serveAuthed(t, router, http.MethodDelete, "/api/v1/builds/pin", nil)
	if rec.Code != http.StatusOK {
		t.Fatalf("unpin status=%d", rec.Code)
	}

	rec = serveAuthed(t, router, http.MethodDelete, "/api/v1/builds/"+buildID, nil)
	if rec.Code != http.StatusOK {
		t.Fatalf("delete status=%d body=%s", rec.Code, rec.Body.String())
	}
}

func TestIntegrationPutConfig(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	rec := serveAuthed(t, router, http.MethodPut, "/api/v1/config", []byte(`{"port":9090}`))
	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
	if !strings.Contains(rec.Body.String(), "9090") {
		t.Fatalf("expected updated config: %s", rec.Body.String())
	}
}

func TestIntegrationBuilderRoutes(t *testing.T) {
	router, _, _, _ := newTestRouter(t)

	rec := serveAuthed(t, router, http.MethodPost, "/api/v1/builder/build", []byte(`{}`))
	if rec.Code == http.StatusNotFound {
		t.Fatal("builder/build route not registered")
	}

	rec = serveAuthed(t, router, http.MethodPost, "/api/v1/builder/estimate", []byte("not-multipart"))
	if rec.Code == http.StatusNotFound {
		t.Fatal("builder/estimate route not registered")
	}
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("estimate without multipart expected 400, got %d", rec.Code)
	}

	rec = serveAuthed(t, router, http.MethodDelete, "/api/v1/builder/cancel/no-such-token", nil)
	if rec.Code != http.StatusNotFound {
		t.Fatalf("cancel missing token expected 404, got %d", rec.Code)
	}
}

func TestIntegrationBlueprintsCRUD(t *testing.T) {
	router, _, _, dataDir := newTestRouter(t)

	saveBody, _ := json.Marshal(map[string]interface{}{
		"name": "integration-preset",
		"data": map[string]interface{}{"threads": 2},
	})
	rec := serveAuthed(t, router, http.MethodPost, "/api/v1/blueprints", saveBody)
	if rec.Code != http.StatusOK {
		t.Fatalf("save status=%d body=%s", rec.Code, rec.Body.String())
	}

	filePath := filepath.Join(dataDir, "blueprints", "integration-preset.json")
	if _, err := os.Stat(filePath); err != nil {
		t.Fatalf("blueprint file missing: %v", err)
	}

	rec = serveAuthed(t, router, http.MethodGet, "/api/v1/blueprints", nil)
	if rec.Code != http.StatusOK {
		t.Fatalf("list status=%d", rec.Code)
	}

	rec = serveAuthed(t, router, http.MethodGet, "/api/v1/blueprints/integration-preset", nil)
	if rec.Code != http.StatusOK {
		t.Fatalf("get status=%d body=%s", rec.Code, rec.Body.String())
	}

	rec = serveAuthed(t, router, http.MethodDelete, "/api/v1/blueprints?name=integration-preset", nil)
	if rec.Code != http.StatusOK {
		t.Fatalf("delete status=%d body=%s", rec.Code, rec.Body.String())
	}
}

func TestIntegrationBlueprintErrors(t *testing.T) {
	router, _, _, _ := newTestRouter(t)

	rec := serveAuthed(t, router, http.MethodPost, "/api/v1/blueprints", []byte(`{"name":"","data":{}}`))
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("empty name expected 400, got %d", rec.Code)
	}

	rec = serveAuthed(t, router, http.MethodGet, "/api/v1/blueprints/missing-preset", nil)
	if rec.Code != http.StatusNotFound {
		t.Fatalf("missing blueprint expected 404, got %d", rec.Code)
	}

	rec = serveAuthed(t, router, http.MethodDelete, "/api/v1/blueprints", nil)
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("delete without name expected 400, got %d", rec.Code)
	}
}

func TestIntegrationAIRoutes(t *testing.T) {
	router, _, _, _ := newTestRouter(t)

	// Agent routes require fleet secret (not Basic Auth).
	SetAgentPathSecret(testFleetSecret)
	t.Cleanup(func() { SetAgentPathSecret("") })

	// decide: missing agent_id → 400.
	rec := serveWithFleetSecret(t, router, http.MethodPost, "/api/v1/agent/decide", testFleetSecret, []byte(`{"worker_name":"x"}`))
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("decide missing agent_id expected 400, got %d body=%s", rec.Code, rec.Body.String())
	}

	// report and heartbeat require fleet secret; they don't need a pre-registered engine.
	body, _ := json.Marshal(map[string]string{"agent_id": "report-agent", "tool": "sleep", "output": "ok"})
	rec = serveWithFleetSecret(t, router, http.MethodPost, "/api/v1/agent/report", testFleetSecret, body)
	if rec.Code != http.StatusOK {
		t.Fatalf("report status=%d body=%s", rec.Code, rec.Body.String())
	}

	hb, _ := json.Marshal(map[string]string{"agent_id": "hb-agent", "status": "alive"})
	rec = serveWithFleetSecret(t, router, http.MethodPost, "/api/v1/agent/heartbeat", testFleetSecret, hb)
	if rec.Code != http.StatusOK {
		t.Fatalf("heartbeat status=%d body=%s", rec.Code, rec.Body.String())
	}
}

func TestIntegrationAgentRoutesFleetSecret(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	SetAgentPathSecret("integration-fleet-secret")
	t.Cleanup(func() { SetAgentPathSecret("") })

	req := httptest.NewRequest(http.MethodPost, "/api/v1/agent/heartbeat",
		bytes.NewReader([]byte(`{"agent_id":"a","status":"alive"}`)))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusForbidden {
		t.Fatalf("missing fleet secret expected 403, got %d", rec.Code)
	}

	req.Header.Set("X-Fleet-Secret", "integration-fleet-secret")
	rec = httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("valid fleet secret expected 200, got %d body=%s", rec.Code, rec.Body.String())
	}
}

func TestIntegrationDropperVariants(t *testing.T) {
	router, _, database, dataDir := newTestRouter(t)

	req := httptest.NewRequest(http.MethodGet, "/get", nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusNotFound {
		t.Fatalf("no builds expected 404, got %d", rec.Code)
	}

	insertTestBuild(t, database, dataDir, "win-drop", "windows", "worker.exe")
	req = httptest.NewRequest(http.MethodGet, "/get?os=windows", nil)
	rec = httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("windows build expected 200, got %d body=%s", rec.Code, rec.Body.String())
	}

	req = httptest.NewRequest(http.MethodGet, "/get?os=linux", nil)
	rec = httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("linux with only windows build falls back to latest, expected 200, got %d", rec.Code)
	}

	req = httptest.NewRequest(http.MethodGet, "/get", nil)
	req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0)")
	rec = httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("UA-detected windows expected 200, got %d", rec.Code)
	}

	for _, path := range []string{"/install.sh", "/install.ps1"} {
		req = httptest.NewRequest(http.MethodGet, path, nil)
		req.Host = "forge.local:8989"
		rec = httptest.NewRecorder()
		router.ServeHTTP(rec, req)
		if rec.Code != http.StatusOK {
			t.Fatalf("%s status=%d", path, rec.Code)
		}
	}
}

func TestIntegrationBuildUninstallNotFound(t *testing.T) {
	router, _, database, dataDir := newTestRouter(t)
	buildID := "uninstall-build"
	insertTestBuild(t, database, dataDir, buildID, "windows", "worker.exe")

	rec := serveAuthed(t, router, http.MethodGet, "/api/v1/builds/"+buildID+"/uninstall", nil)
	if rec.Code != http.StatusNotFound {
		t.Fatalf("missing uninstall script expected 404, got %d", rec.Code)
	}
}

func TestIntegrationRouterWebSocketDashboard(t *testing.T) {
	router, _, _, _ := newTestRouter(t)
	srv := startRouterServer(t, router)

	badURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/ws/dashboard"
	_, resp, err := websocket.DefaultDialer.Dial(badURL, nil)
	if err == nil {
		t.Fatal("expected dial failure without token")
	}
	if resp == nil || resp.StatusCode != http.StatusUnauthorized {
		t.Fatalf("expected 401 without token, got err=%v status=%v", err, resp)
	}

	goodURL := badURL + "?token=" + wsDashboardToken(testAuthUser, testAuthPass)
	conn, resp, err := websocket.DefaultDialer.Dial(goodURL, nil)
	if err != nil {
		t.Fatalf("dial with token: %v status=%v", err, resp)
	}
	t.Cleanup(func() { _ = conn.Close() })

	var msg Message
	if err := conn.ReadJSON(&msg); err != nil {
		t.Fatalf("read init: %v", err)
	}
	if msg.Type != "init" {
		t.Fatalf("expected init, got %q", msg.Type)
	}
}

func TestIntegrationRouterWebSocketAgentBadSecret(t *testing.T) {
	router, wsHub, _, _ := newTestRouter(t)
	wsHub.SetFleetSecret("ws-fleet-secret")
	t.Cleanup(func() { wsHub.SetFleetSecret("") })

	srv := startRouterServer(t, router)
	wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/ws/agent"
	conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
	if err != nil {
		t.Fatalf("dial agent ws: %v", err)
	}
	t.Cleanup(func() { _ = conn.Close() })

	resp := authAgentConn(t, conn, map[string]interface{}{
		"agent_id": "bad-secret-agent", "fleet_secret": "wrong", "hostname": "host",
	})
	var body map[string]interface{}
	if err := json.Unmarshal(resp.Payload, &body); err != nil {
		t.Fatal(err)
	}
	if body["success"] != false {
		t.Fatalf("expected auth failure with bad fleet secret, got %+v", body)
	}
	if wsHub.isAgentConnected("bad-secret-agent") {
		t.Fatal("agent should not register with bad fleet secret")
	}
}

func TestIntegrationRouterWebSocketAgentConnectedCommand(t *testing.T) {
	router, wsHub, _, _ := newTestRouter(t)
	agentID := "router-cmd-agent"
	connectAgentViaRouter(t, router, agentID)

	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		if wsHub.isAgentConnected(agentID) {
			break
		}
		time.Sleep(10 * time.Millisecond)
	}
	if !wsHub.isAgentConnected(agentID) {
		t.Fatal("agent not connected via router ws")
	}

	rec := serveAuthed(t, router, http.MethodPost, "/api/v1/agents/"+agentID+"/command",
		[]byte(`{"action":"pause"}`))
	if rec.Code != http.StatusOK {
		t.Fatalf("command status=%d body=%s", rec.Code, rec.Body.String())
	}
}
