//go:build testing

package heartbeat_test

import (
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/henrygd/beszel/internal/hub/heartbeat"
	beszeltests "github.com/henrygd/beszel/internal/tests"
	"github.com/pocketbase/pocketbase/core"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestNew(t *testing.T) {
	t.Run("returns nil when app is missing", func(t *testing.T) {
		hb := heartbeat.New(nil, envGetter(map[string]string{
			"HEARTBEAT_URL": "https://heartbeat.example.com/ping",
		}))
		assert.Nil(t, hb)
	})

	t.Run("returns nil when URL is missing", func(t *testing.T) {
		app := newTestHub(t)
		hb := heartbeat.New(app.App, func(string) (string, bool) {
			return "", false
		})
		assert.Nil(t, hb)
	})

	t.Run("parses and normalizes config values", func(t *testing.T) {
		app := newTestHub(t)
		env := map[string]string{
			"HEARTBEAT_URL":      "  https://heartbeat.example.com/ping  ",
			"HEARTBEAT_INTERVAL": "90",
			"HEARTBEAT_METHOD":   "head",
		}
		getEnv := func(key string) (string, bool) {
			v, ok := env[key]
			return v, ok
		}

		hb := heartbeat.New(app.App, getEnv)
		require.NotNil(t, hb)
		cfg := hb.GetConfig()
		assert.Equal(t, "https://heartbeat.example.com/ping", cfg.URL)
		assert.Equal(t, 90, cfg.Interval)
		assert.Equal(t, http.MethodHead, cfg.Method)
	})
}

func TestSendGETDoesNotRequireAppOrDB(t *testing.T) {
	app := newTestHub(t)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		assert.Equal(t, http.MethodGet, r.Method)
		assert.Equal(t, "Beszel-Heartbeat", r.Header.Get("User-Agent"))
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	hb := heartbeat.New(app.App, envGetter(map[string]string{
		"HEARTBEAT_URL":    server.URL,
		"HEARTBEAT_METHOD": "GET",
	}))
	require.NotNil(t, hb)

	require.NoError(t, hb.Send())
}

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

	hb := heartbeat.New(app.App, envGetter(map[string]string{
		"HEARTBEAT_URL":    server.URL,
		"HEARTBEAT_METHOD": "GET",
	}))
	require.NotNil(t, hb)

	err := hb.Send()
	require.Error(t, err)
	assert.ErrorContains(t, err, "heartbeat endpoint returned status 500")
}

func TestSendPOSTBuildsExpectedStatuses(t *testing.T) {
	tests := []struct {
		name           string
		setup          func(t *testing.T, app *beszeltests.TestHub, user *core.Record)
		expectStatus   string
		expectMsgPart  string
		expectDown     int
		expectAlerts   int
		expectTotal    int
		expectUp       int
		expectPaused   int
		expectPending  int
		expectDownSumm int
	}{
		{
			name: "error when at least one system is down",
			setup: func(t *testing.T, app *beszeltests.TestHub, user *core.Record) {
				downSystem := createTestSystem(t, app, user.Id, "db-1", "10.0.0.1", "down")
				_ = createTestSystem(t, app, user.Id, "web-1", "10.0.0.2", "up")
				createTriggeredAlert(t, app, user.Id, downSystem.Id, "CPU", 95)
			},
			expectStatus:   "error",
			expectMsgPart:  "1 system(s) down",
			expectDown:     1,
			expectAlerts:   1,
			expectTotal:    2,
			expectUp:       1,
			expectDownSumm: 1,
		},
		{
			name: "warn when only alerts are triggered",
			setup: func(t *testing.T, app *beszeltests.TestHub, user *core.Record) {
				system := createTestSystem(t, app, user.Id, "api-1", "10.1.0.1", "up")
				createTriggeredAlert(t, app, user.Id, system.Id, "CPU", 90)
			},
			expectStatus:   "warn",
			expectMsgPart:  "1 alert(s) triggered",
			expectDown:     0,
			expectAlerts:   1,
			expectTotal:    1,
			expectUp:       1,
			expectDownSumm: 0,
		},
		{
			name: "ok when no down systems and no alerts",
			setup: func(t *testing.T, app *beszeltests.TestHub, user *core.Record) {
				_ = createTestSystem(t, app, user.Id, "node-1", "10.2.0.1", "up")
				_ = createTestSystem(t, app, user.Id, "node-2", "10.2.0.2", "paused")
				_ = createTestSystem(t, app, user.Id, "node-3", "10.2.0.3", "pending")
			},
			expectStatus:   "ok",
			expectMsgPart:  "All systems operational",
			expectDown:     0,
			expectAlerts:   0,
			expectTotal:    3,
			expectUp:       1,
			expectPaused:   1,
			expectPending:  1,
			expectDownSumm: 0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			app := newTestHub(t)
			user := createTestUser(t, app)
			tt.setup(t, app, user)

			type requestCapture struct {
				method      string
				userAgent   string
				contentType string
				payload     heartbeat.Payload
			}

			captured := make(chan requestCapture, 1)
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				defer r.Body.Close()
				body, err := io.ReadAll(r.Body)
				require.NoError(t, err)

				var payload heartbeat.Payload
				require.NoError(t, json.Unmarshal(body, &payload))
				captured <- requestCapture{
					method:      r.Method,
					userAgent:   r.Header.Get("User-Agent"),
					contentType: r.Header.Get("Content-Type"),
					payload:     payload,
				}
				w.WriteHeader(http.StatusNoContent)
			}))
			defer server.Close()

			hb := heartbeat.New(app.App, envGetter(map[string]string{
				"HEARTBEAT_URL":    server.URL,
				"HEARTBEAT_METHOD": "POST",
			}))
			require.NotNil(t, hb)
			require.NoError(t, hb.Send())

			req := <-captured
			assert.Equal(t, http.MethodPost, req.method)
			assert.Equal(t, "Beszel-Heartbeat", req.userAgent)
			assert.Equal(t, "application/json", req.contentType)

			assert.Equal(t, tt.expectStatus, req.payload.Status)
			assert.Contains(t, req.payload.Msg, tt.expectMsgPart)
			assert.Equal(t, tt.expectDown, len(req.payload.Down))
			assert.Equal(t, tt.expectAlerts, len(req.payload.Alerts))
			assert.Equal(t, tt.expectTotal, req.payload.Systems.Total)
			assert.Equal(t, tt.expectUp, req.payload.Systems.Up)
			assert.Equal(t, tt.expectDownSumm, req.payload.Systems.Down)
			assert.Equal(t, tt.expectPaused, req.payload.Systems.Paused)
			assert.Equal(t, tt.expectPending, req.payload.Systems.Pending)
		})
	}
}

func newTestHub(t *testing.T) *beszeltests.TestHub {
	t.Helper()
	app, err := beszeltests.NewTestHub(t.TempDir())
	require.NoError(t, err)
	t.Cleanup(app.Cleanup)
	return app
}

func createTestUser(t *testing.T, app *beszeltests.TestHub) *core.Record {
	t.Helper()
	user, err := beszeltests.CreateUser(app.App, "admin@example.com", "password123")
	require.NoError(t, err)
	return user
}

func createTestSystem(t *testing.T, app *beszeltests.TestHub, userID, name, host, status string) *core.Record {
	t.Helper()
	system, err := beszeltests.CreateRecord(app.App, "systems", map[string]any{
		"name":   name,
		"host":   host,
		"port":   "45876",
		"users":  []string{userID},
		"status": status,
	})
	require.NoError(t, err)
	return system
}

func createTriggeredAlert(t *testing.T, app *beszeltests.TestHub, userID, systemID, name string, threshold float64) *core.Record {
	t.Helper()
	alert, err := beszeltests.CreateRecord(app.App, "alerts", map[string]any{
		"name":      name,
		"system":    systemID,
		"user":      userID,
		"value":     threshold,
		"min":       0,
		"triggered": true,
	})
	require.NoError(t, err)
	return alert
}

func envGetter(values map[string]string) func(string) (string, bool) {
	return func(key string) (string, bool) {
		v, ok := values[key]
		return v, ok
	}
}
