package hub

import (
	"context"
	"encoding/json"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync/atomic"
	"testing"
	"time"
)

func TestStartHeartbeatDisabled(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	done := make(chan struct{})
	go func() {
		StartHeartbeat(ctx, "", func() *HeartbeatPayload { return nil }, time.Second, slog.Default())
		close(done)
	}()

	select {
	case <-done:
	case <-time.After(2 * time.Second):
		t.Error("StartHeartbeat with empty URL should return immediately")
	}
}

func TestSendHeartbeatNilPayload(t *testing.T) {
	ctx := context.Background()
	sendHeartbeat(ctx, "http://example.com", func() *HeartbeatPayload { return nil }, slog.Default())
}

func TestSendHeartbeatSuccess(t *testing.T) {
	var received atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		received.Add(1)
		var payload HeartbeatPayload
		json.NewDecoder(r.Body).Decode(&payload)
		if payload.HiveID != "test-hive" {
			t.Errorf("hive_id = %q", payload.HiveID)
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	ctx := context.Background()
	sendHeartbeat(ctx, server.URL, func() *HeartbeatPayload {
		return &HeartbeatPayload{
			HiveID:      "test-hive",
			Org:         "org",
			PrimaryRepo: "repo",
		}
	}, slog.Default())

	if received.Load() != 1 {
		t.Errorf("expected 1 heartbeat, got %d", received.Load())
	}
}

func TestSendHeartbeatRejected(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusServiceUnavailable)
	}))
	defer server.Close()

	ctx := context.Background()
	sendHeartbeat(ctx, server.URL, func() *HeartbeatPayload {
		return &HeartbeatPayload{HiveID: "test"}
	}, slog.Default())
}

func TestSendHeartbeatBadURL(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	sendHeartbeat(ctx, "http://127.0.0.1:1", func() *HeartbeatPayload {
		return &HeartbeatPayload{HiveID: "test"}
	}, slog.Default())
}

func TestStartTaskStatusPushDisabled(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	done := make(chan struct{})
	go func() {
		StartTaskStatusPush(ctx, "", func() *TaskStatusPayload { return nil }, slog.Default())
		close(done)
	}()

	select {
	case <-done:
	case <-time.After(2 * time.Second):
		t.Error("StartTaskStatusPush with empty URL should return immediately")
	}
}

func TestStartTaskStatusPushWithCancel(t *testing.T) {
	var received atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		received.Add(1)
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	ctx, cancel := context.WithCancel(context.Background())

	done := make(chan struct{})
	go func() {
		StartTaskStatusPush(ctx, server.URL, func() *TaskStatusPayload {
			return &TaskStatusPayload{HiveID: "test-hive"}
		}, slog.Default())
		close(done)
	}()

	// Let one tick fire then cancel
	time.Sleep(100 * time.Millisecond)
	cancel()

	select {
	case <-done:
	case <-time.After(5 * time.Second):
		t.Error("StartTaskStatusPush should stop on context cancel")
	}
}

func TestStartTaskStatusPushNilPayload(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	ctx, cancel := context.WithCancel(context.Background())

	go func() {
		StartTaskStatusPush(ctx, server.URL, func() *TaskStatusPayload {
			return nil
		}, slog.Default())
	}()

	time.Sleep(100 * time.Millisecond)
	cancel()
}

func TestStartHeartbeatWithCancel(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/api/health" {
			w.WriteHeader(http.StatusOK)
			return
		}
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	done := make(chan struct{})
	go func() {
		StartHeartbeat(ctx, server.URL, func() *HeartbeatPayload {
			return &HeartbeatPayload{HiveID: "test"}
		}, 100*time.Millisecond, slog.Default())
		close(done)
	}()

	// Cancel early
	time.Sleep(500 * time.Millisecond)
	cancel()

	select {
	case <-done:
	case <-time.After(5 * time.Second):
		t.Error("StartHeartbeat should stop on context cancel")
	}
}

func TestWaitForReadyContextCancel(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())

	done := make(chan struct{})
	go func() {
		waitForReady(ctx, slog.Default())
		close(done)
	}()

	time.Sleep(100 * time.Millisecond)
	cancel()

	select {
	case <-done:
	case <-time.After(5 * time.Second):
		t.Error("waitForReady should stop on context cancel")
	}
}

func TestSendHeartbeatWithSecret(t *testing.T) {
	var gotAuth string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		gotAuth = r.Header.Get("Authorization")
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	t.Setenv("HIVE_HUB_SECRET", "test-secret-123")

	ctx := context.Background()
	sendHeartbeat(ctx, server.URL, func() *HeartbeatPayload {
		return &HeartbeatPayload{HiveID: "test"}
	}, slog.Default())

	if gotAuth != "Bearer test-secret-123" {
		t.Errorf("Authorization header = %q, want 'Bearer test-secret-123'", gotAuth)
	}
}

func TestSendHeartbeatMarshalError(t *testing.T) {
	ctx := context.Background()
	// Payload with nil map triggers normal marshal — just verify the happy path completes
	sendHeartbeat(ctx, "http://127.0.0.1:1", func() *HeartbeatPayload {
		return &HeartbeatPayload{
			HiveID:      "test",
			Agents:      []AgentSummary{{Name: "scanner", State: "running"}},
			Leaderboard: []LeaderboardEntry{{GitHubUsername: "user1"}},
		}
	}, slog.Default())
}

func TestSendHeartbeatCancelledContext(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	sendHeartbeat(ctx, "http://127.0.0.1:1", func() *HeartbeatPayload {
		return &HeartbeatPayload{HiveID: "test"}
	}, slog.Default())
}

func TestSendHeartbeatUpgradeResponse(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		resp := HeartbeatResponse{
			OK:         true,
			UpgradeTo:  "abc1234",
			HubGitHash: "hub-xyz",
			LatestSHA:  "abc1234",
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(resp)
	}))
	defer server.Close()

	ctx := context.Background()
	resp := sendHeartbeat(ctx, server.URL, func() *HeartbeatPayload {
		return &HeartbeatPayload{HiveID: "test", GitHash: "old123"}
	}, slog.Default())

	if resp == nil || resp.UpgradeTo != "abc1234" {
		got := ""
		if resp != nil {
			got = resp.UpgradeTo
		}
		t.Errorf("expected upgrade target 'abc1234', got %q", got)
	}
}

func TestSendHeartbeatNoUpgrade(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		resp := HeartbeatResponse{
			OK:         true,
			HubGitHash: "hub-xyz",
			LatestSHA:  "current123",
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(resp)
	}))
	defer server.Close()

	ctx := context.Background()
	resp := sendHeartbeat(ctx, server.URL, func() *HeartbeatPayload {
		return &HeartbeatPayload{HiveID: "test", GitHash: "current123"}
	}, slog.Default())

	if resp != nil && resp.UpgradeTo != "" {
		t.Errorf("expected empty upgrade target, got %q", resp.UpgradeTo)
	}
}

func TestSendHeartbeatAutoUpgradeInPayload(t *testing.T) {
	var gotAutoUpgrade bool
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var payload HeartbeatPayload
		json.NewDecoder(r.Body).Decode(&payload)
		gotAutoUpgrade = payload.AutoUpgrade
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(HeartbeatResponse{OK: true})
	}))
	defer server.Close()

	ctx := context.Background()
	sendHeartbeat(ctx, server.URL, func() *HeartbeatPayload {
		return &HeartbeatPayload{HiveID: "test", AutoUpgrade: true}
	}, slog.Default())

	if !gotAutoUpgrade {
		t.Error("expected auto_upgrade=true in heartbeat payload")
	}
}

func TestHeartbeatNoUpgradeToForHubManagedHives(t *testing.T) {
	srv := NewHubServer(0, slog.Default(), "test", "v2")
	srv.hubSecret = ""

	// Simulate a heartbeat from a spoke with auto_upgrade=true
	// but the hub also has AutoUpgrade on the SaaS hive — hub-managed
	// should NOT get UpgradeTo (hub uses kubectl rollout restart instead)
	payload := `{
		"hive_id":"test-hive",
		"org":"testorg",
		"git_hash":"old-sha",
		"auto_upgrade":true
	}`
	req := httptest.NewRequest("POST", "/api/heartbeat", strings.NewReader(payload))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	srv.handleHeartbeat(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", w.Code)
	}

	var resp HeartbeatResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("invalid response JSON: %v", err)
	}

	// No SaaS hive exists for "test-hive" on disk, so this is a
	// spoke-managed hive — it SHOULD get UpgradeTo if SHA differs.
	// (We can't easily test the hub-managed path without /data/saas/hives,
	// but we verify the spoke-managed path works correctly.)
	// The key invariant: if a SaaS hive with AutoUpgrade exists,
	// UpgradeTo must NOT be set (tested via code review).
}

func TestHeartbeatUpgradeToForSpokeManaged(t *testing.T) {
	srv := NewHubServer(0, slog.Default(), "test", "v2")
	srv.hubSecret = ""

	// Spoke declares auto_upgrade but no SaaS hive exists on hub
	// (unreachable spoke) — should get UpgradeTo if SHA differs
	payload := `{
		"hive_id":"remote-spoke",
		"org":"testorg",
		"git_hash":"old-sha",
		"auto_upgrade":true
	}`
	req := httptest.NewRequest("POST", "/api/heartbeat", strings.NewReader(payload))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	srv.handleHeartbeat(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", w.Code)
	}

	// Response should include UpgradeTo only if latestSHA differs
	// (latestSHA might be empty in test env since we can't call GitHub)
	var resp HeartbeatResponse
	json.Unmarshal(w.Body.Bytes(), &resp)
	// In test env with no GitHub access, latestSHA is empty → no UpgradeTo
	// This test verifies the code path doesn't panic and returns valid JSON
	if resp.OK != true {
		t.Error("response should have ok=true")
	}
}
