package metrics

import (
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/testutil"
	dto "github.com/prometheus/client_model/go"
	"github.com/stretchr/testify/require"
)

// histCount reads a histogram child's observation count (testutil.ToFloat64 only
// works on counters/gauges; a histogram's WithLabelValues child is an Observer).
func histCount(t *testing.T, o prometheus.Observer) uint64 {
	t.Helper()
	m, ok := o.(prometheus.Metric)
	require.True(t, ok, "histogram child must be a prometheus.Metric")
	var d dto.Metric
	require.NoError(t, m.Write(&d))
	return d.GetHistogram().GetSampleCount()
}

// TestObserveSummarizeRecordsModelOutcomeFallback: a success observation lands on
// the right model/outcome/fallback series.
func TestObserveSummarizeRecordsModelOutcomeFallback(t *testing.T) {
	before := histCount(t, summarizeDuration.WithLabelValues("koala/phi4-mini", "success", "false"))
	ObserveSummarize("koala/phi4-mini", "success", false, 1200*time.Millisecond)
	after := histCount(t, summarizeDuration.WithLabelValues("koala/phi4-mini", "success", "false"))
	require.Equal(t, before+1, after, "one success observation recorded for the model")
}

// TestObserveSummarizeRecordsFailureOutcomes: error and parse_error are distinct
// series so a fallback chain's failures are visible.
func TestObserveSummarizeRecordsFailureOutcomes(t *testing.T) {
	e0 := histCount(t, summarizeDuration.WithLabelValues("m", "error", "false"))
	p0 := histCount(t, summarizeDuration.WithLabelValues("m", "parse_error", "false"))
	ObserveSummarize("m", "error", false, time.Second)
	ObserveSummarize("m", "parse_error", false, time.Second)
	require.Equal(t, e0+1, histCount(t, summarizeDuration.WithLabelValues("m", "error", "false")))
	require.Equal(t, p0+1, histCount(t, summarizeDuration.WithLabelValues("m", "parse_error", "false")))
}

func TestObserveCaptionFetchByOutcome(t *testing.T) {
	b := histCount(t, captionFetchDuration.WithLabelValues("captions"))
	ObserveCaptionFetch("captions", 3*time.Second)
	require.Equal(t, b+1, histCount(t, captionFetchDuration.WithLabelValues("captions")))
}

func TestChatAnswerLatencyRecorded(t *testing.T) {
	b := histCount(t, chatDuration.WithLabelValues("iguana/gemma4-26b"))
	ObserveChat("iguana/gemma4-26b", 2*time.Second)
	require.Equal(t, b+1, histCount(t, chatDuration.WithLabelValues("iguana/gemma4-26b")))
}

// TestRecordTokens: prompt + completion land on their kind series; zero is skipped.
func TestRecordTokens(t *testing.T) {
	p0 := testutil.ToFloat64(llmTokens.WithLabelValues("m", "prompt"))
	c0 := testutil.ToFloat64(llmTokens.WithLabelValues("m", "completion"))
	RecordTokens("m", 100, 40)
	RecordTokens("m", 0, 0) // skipped, no panic
	require.Equal(t, p0+100, testutil.ToFloat64(llmTokens.WithLabelValues("m", "prompt")))
	require.Equal(t, c0+40, testutil.ToFloat64(llmTokens.WithLabelValues("m", "completion")))
}

func TestLoginCounted(t *testing.T) {
	b := testutil.ToFloat64(logins)
	IncLogin()
	require.Equal(t, b+1, testutil.ToFloat64(logins))
}

// TestHTTPMiddlewareRecordsByRoutePattern: the request is counted under the bounded
// registered pattern (r.Pattern after routing), not the raw path with its ids.
func TestHTTPMiddlewareRecordsByRoutePattern(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /v/{videoId}", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusTeapot)
	})
	h := HTTPMiddleware(mux)

	before := testutil.ToFloat64(httpRequests.WithLabelValues("GET", "GET /v/{videoId}", "418"))
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/v/abc-123", nil))

	require.Equal(t, http.StatusTeapot, rec.Code)
	after := testutil.ToFloat64(httpRequests.WithLabelValues("GET", "GET /v/{videoId}", "418"))
	require.Equal(t, before+1, after, "counted under the pattern, not /v/abc-123")
	require.Equal(t, float64(0), testutil.ToFloat64(httpRequests.WithLabelValues("GET", "/v/abc-123", "418")),
		"raw path must never be a label value")
}
