package classify

import (
	"context"
	"errors"
	"io"
	"log/slog"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/alertstore/pkg/alert"
)

func TestClinicActivityDowngradesPage(t *testing.T) {
	t.Parallel()

	observedAt := time.Unix(1700000000, 0)
	clinic := &clinicStub{matched: true}
	classifier := New(nil, clinic, discardLogger())

	result := classifier.Classify(
		context.Background(),
		"TiDBClusterFrequentlyRestart",
		alert.ClassPage,
		"",
		"tenant-1",
		"cluster-1",
		observedAt,
	)

	require.Equal(t, alert.ClassTicket, result.Classification)
	require.Equal(t, alert.ClassPage, result.Original)
	require.Equal(t, "recent_activity", result.Reason)
	require.Equal(t, "TiDBClusterFrequentlyRestart", clinic.alertname)
	require.Equal(t, "cluster-1", clinic.clusterID)
	require.Equal(t, observedAt, clinic.observedAt)
}

func TestClinicActivityFailureKeepsPage(t *testing.T) {
	t.Parallel()

	classifier := New(nil, &clinicStub{err: errors.New("clinic unavailable")}, discardLogger())
	result := classifier.Classify(
		context.Background(),
		"TiDBClusterFrequentlyRestart",
		alert.ClassPage,
		"",
		"",
		"cluster-1",
		time.Now(),
	)

	require.Equal(t, alert.ClassPage, result.Classification)
	require.Empty(t, result.Reason)
}

func TestClinicActivityDoesNotReclassifyTickets(t *testing.T) {
	t.Parallel()

	clinic := &clinicStub{matched: true}
	classifier := New(nil, clinic, discardLogger())
	result := classifier.Classify(
		context.Background(),
		"TiDBClusterFrequentlyRestart",
		alert.ClassTicket,
		"",
		"",
		"cluster-1",
		time.Now(),
	)

	require.Equal(t, alert.ClassTicket, result.Classification)
	require.Empty(t, clinic.alertname)
}

type clinicStub struct {
	matched    bool
	err        error
	alertname  string
	clusterID  string
	observedAt time.Time
}

func (s *clinicStub) HasRelatedActivity(
	_ context.Context,
	alertname string,
	clusterID string,
	observedAt time.Time,
) (bool, error) {
	s.alertname = alertname
	s.clusterID = clusterID
	s.observedAt = observedAt
	return s.matched, s.err
}

func discardLogger() *slog.Logger {
	return slog.New(slog.NewTextHandler(io.Discard, nil))
}

// A customer-facing rule is never an internal response. The override runs
// first and returns outright, so nothing downstream can raise it back.
func TestExternalVisibilityIsAlwaysLogging(t *testing.T) {
	t.Parallel()

	clinic := &clinicStub{matched: false}
	classifier := New(nil, clinic, discardLogger())

	for _, class := range []string{alert.ClassPage, alert.ClassTicket, alert.ClassLogging, ""} {
		result := classifier.Classify(
			context.Background(),
			"TiDBStorageAlmostFull",
			class,
			alert.VisibilityExternal,
			"tenant-1",
			"cluster-1",
			time.Now(),
		)

		require.Equal(t, alert.ClassLogging, result.Classification)
		require.Equal(t, class, result.Original)
		require.Equal(t, "external_visibility", result.Reason)
	}

	require.Empty(t, clinic.alertname, "a customer-facing alert needs no activity precheck")
}

// Absent means internal: a rule that says nothing about its audience keeps
// whatever response tier it asked for.
func TestAbsentVisibilityLeavesClassificationAlone(t *testing.T) {
	t.Parallel()

	classifier := New(nil, &clinicStub{}, discardLogger())
	result := classifier.Classify(
		context.Background(),
		"TiKVInstanceLowSpace",
		alert.ClassPage,
		"",
		"tenant-1",
		"cluster-1",
		time.Now(),
	)

	require.Equal(t, alert.ClassPage, result.Classification)
	require.Empty(t, result.Reason)
}
