package observability_test

import (
	"context"
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/stretchr/testify/require"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/observability"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/mock_modserver"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/testlogger"
	"go.uber.org/mock/gomock"
)

func TestMetricServer(t *testing.T) {
	ctrl := gomock.NewController(t)
	listener, err := net.Listen("tcp", "localhost:0")
	require.NoError(t, err)
	defer listener.Close()
	logger := testlogger.New(t)
	mockAPI := mock_modserver.NewMockAPI(ctrl)
	probeRegistry := observability.NewProbeRegistry()
	metricSrv := observability.MetricServer{
		API:                   mockAPI,
		Log:                   logger,
		Name:                  "test-server",
		Listener:              listener,
		PrometheusURLPath:     "/metrics",
		LivenessProbeURLPath:  "/liveness",
		ReadinessProbeURLPath: "/readiness",
		Gatherer:              prometheus.DefaultGatherer,
		Registerer:            prometheus.DefaultRegisterer,
		ProbeRegistry:         probeRegistry,
	}
	handler := metricSrv.ConstructHandler()

	httpGet := func(t *testing.T, path string) *httptest.ResponseRecorder {
		request, err := http.NewRequest("GET", path, nil) //nolint:noctx
		require.NoError(t, err)
		recorder := httptest.NewRecorder()
		handler.ServeHTTP(recorder, request)
		return recorder
	}

	// tests

	t.Run("/metrics", func(t *testing.T) {
		httpResponse := httpGet(t, "/metrics").Result()
		require.Equal(t, http.StatusOK, httpResponse.StatusCode)
		httpResponse.Body.Close()
	})

	t.Run("/liveness", func(t *testing.T) {
		// succeeds when there are no probes
		rec := httpGet(t, "/liveness")
		httpResponse := rec.Result()
		require.Equal(t, http.StatusOK, httpResponse.StatusCode)
		require.Empty(t, rec.Body)
		httpResponse.Body.Close()

		// fails when a probe fails
		probeErr := errors.New("failed liveness on purpose")
		expectedErr := fmt.Errorf("test-liveness: %w", probeErr)
		probeRegistry.RegisterLivenessProbe("test-liveness", func(ctx context.Context) error {
			return probeErr
		})
		mockAPI.EXPECT().
			HandleProcessingError(gomock.Any(), gomock.Any(), "LivenessProbe failed", expectedErr)
		rec = httpGet(t, "/liveness")
		httpResponse = rec.Result()
		require.Equal(t, http.StatusInternalServerError, httpResponse.StatusCode)
		require.Equal(t, "test-liveness: failed liveness on purpose\n", rec.Body.String())
		httpResponse.Body.Close()
	})

	t.Run("/readiness", func(t *testing.T) {
		markReady := probeRegistry.RegisterReadinessToggle("test-readiness-toggle")

		// fails when toggle has not been called
		expectedErr := fmt.Errorf("test-readiness-toggle: %w", errors.New("not ready yet"))
		mockAPI.EXPECT().HandleProcessingError(gomock.Any(), gomock.Any(), "ReadinessProbe failed", expectedErr)
		rec := httpGet(t, "/readiness")
		httpResponse := rec.Result()
		require.Equal(t, http.StatusInternalServerError, httpResponse.StatusCode)
		require.Equal(t, "test-readiness-toggle: not ready yet\n", rec.Body.String())
		httpResponse.Body.Close()

		// succeeds when toggle has been called
		markReady()
		rec = httpGet(t, "/readiness")
		httpResponse = rec.Result()
		require.Equal(t, http.StatusOK, httpResponse.StatusCode)
		require.Empty(t, rec.Body)
		httpResponse.Body.Close()

		// fails when a probe fails
		probeErr := errors.New("failed readiness on purpose")
		expectedErr = fmt.Errorf("test-readiness: %w", probeErr)
		probeRegistry.RegisterReadinessProbe("test-readiness", func(ctx context.Context) error {
			return probeErr
		})
		mockAPI.EXPECT().
			HandleProcessingError(gomock.Any(), gomock.Any(), "ReadinessProbe failed", expectedErr)

		rec = httpGet(t, "/readiness")
		httpResponse = rec.Result()
		require.Equal(t, http.StatusInternalServerError, httpResponse.StatusCode)
		require.Equal(t, "test-readiness: failed readiness on purpose\n", rec.Body.String())
		httpResponse.Body.Close()
	})
}
