package clinic

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

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

	var calls atomic.Int32
	httpClient := newTestHTTPClient(func(r *http.Request) *http.Response {
		calls.Add(1)
		require.Equal(t, "/clinic/api/v1/dashboard/clusters2", r.URL.Path)
		require.Equal(t, "1379661944646416024", r.URL.Query().Get("cluster_id"))
		require.Equal(t, "Bearer clinic-token", r.Header.Get("Authorization"))
		return jsonResponse(http.StatusOK, `{
			"items": [{
				"tenantID": "1001",
				"clusterName": "production",
				"clusterVersion": "v7.5.2",
				"clusterDeployTypeV2": "premium resource pool"
			}]
		}`)
	})

	client, err := New(Config{
		BaseURL: "https://clinic.example.com",
		Token:   "clinic-token",
	}, httpClient)
	require.NoError(t, err)

	cluster, err := client.GetCluster(context.Background(), "1379661944646416024")
	require.NoError(t, err)
	require.Equal(t, Cluster{
		TenantID:   "1001",
		Name:       "production",
		Version:    "v7.5.2",
		DeployType: "Premium",
	}, cluster)

	cached, err := client.GetCluster(context.Background(), "1379661944646416024")
	require.NoError(t, err)
	require.Equal(t, cluster, cached)
	require.Equal(t, int32(1), calls.Load())
}

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

	client, err := New(Config{
		BaseURL: "https://clinic.pingcap.com",
	}, nil)
	require.NoError(t, err)

	require.Equal(t,
		"https://clinic.pingcap.com/portal/#/clusters?query=123",
		client.PortalURL("123"),
	)
}

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

	var calls atomic.Int32
	httpClient := newTestHTTPClient(func(r *http.Request) *http.Response {
		calls.Add(1)
		require.Equal(t, "eventbus", r.Header.Get("x-activityhub-source"))
		require.Equal(t, "event-key", r.Header.Get("x-activityhub-key"))
		return jsonResponse(http.StatusOK, `{
			"result": [{
				"display_name": "ModifyClusterSize",
				"calibration_time": 1700000000,
				"tags": {"status": "succeeded"}
			}]
		}`)
	})

	client, err := New(Config{
		ActivityURL:    "https://activity.example.com/activities/",
		ActivitySource: "eventbus",
		ActivityKey:    "event-key",
	}, httpClient)
	require.NoError(t, err)

	matched, err := client.HasRelatedActivity(
		context.Background(),
		"TiDBClusterFrequentlyRestart",
		"123",
		time.Unix(1700003600, 0),
	)
	require.NoError(t, err)
	require.True(t, matched)
	require.Equal(t, int32(1), calls.Load())
}

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

	var calls atomic.Int32
	httpClient := newTestHTTPClient(func(_ *http.Request) *http.Response {
		call := calls.Add(1)
		if call == 1 {
			return jsonResponse(http.StatusOK, `{"result":[]}`)
		}
		// Deliberately reverse the response order. The client sorts by event
		// time before deciding whether the operation is still open.
		return jsonResponse(http.StatusOK, `{
			"result": [
				{"display_name":"configchange:finish","calibration_time":200},
				{"display_name":"configchange:start","calibration_time":100}
			]
		}`)
	})

	client, err := New(Config{
		ActivityURL:    "https://activity.example.com/activities/",
		ActivitySource: "eventbus",
		ActivityKey:    "event-key",
	}, httpClient)
	require.NoError(t, err)

	matched, err := client.HasRelatedActivity(
		context.Background(),
		"TiKVServerFrequentlyRestart",
		"123",
		time.Unix(1700003600, 0),
	)
	require.NoError(t, err)
	require.False(t, matched)
	require.Equal(t, int32(2), calls.Load())
}

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

	client, err := New(Config{}, nil)
	require.NoError(t, err)

	matched, err := client.HasRelatedActivity(
		context.Background(),
		"TiDBDown",
		"123",
		time.Now(),
	)
	require.NoError(t, err)
	require.False(t, matched)
}

// Clinic is keyed by the cluster number. The cluster labels also carry names —
// the fleet's own infrastructure is monitored through the same Alertmanager and
// alert.Fact falls back to `namespace` — and asking about one is a request that
// can only come back empty.
func TestGetClusterAsksNothingWithoutANumericClusterID(t *testing.T) {
	t.Parallel()

	var calls atomic.Int32
	httpClient := newTestHTTPClient(func(_ *http.Request) *http.Response {
		calls.Add(1)
		return jsonResponse(http.StatusOK, `{"items":[]}`)
	})

	client, err := New(Config{
		BaseURL: "https://clinic.example.com",
		Token:   "clinic-token",
	}, httpClient)
	require.NoError(t, err)

	for _, clusterID := range []string{"", "  ", "infra-dev", "tidb-serverless", "tidb123", "12 3"} {
		cluster, err := client.GetCluster(context.Background(), clusterID)
		require.NoError(t, err, clusterID)
		require.Equal(t, Cluster{}, cluster, clusterID)
	}
	require.Equal(t, int32(0), calls.Load())

	// A numbered cluster is still asked about, surrounding space and all.
	_, err = client.GetCluster(context.Background(), " 123 ")
	require.NoError(t, err)
	require.Equal(t, int32(1), calls.Load())
}

// The same holds for the activity precheck, which runs on every page: its
// cluster ID goes straight into the ActivityHub path.
func TestHasRelatedActivityAsksNothingWithoutANumericClusterID(t *testing.T) {
	t.Parallel()

	var calls atomic.Int32
	httpClient := newTestHTTPClient(func(_ *http.Request) *http.Response {
		calls.Add(1)
		return jsonResponse(http.StatusOK, `{"result":[]}`)
	})

	client, err := New(Config{
		ActivityURL:    "https://activity.example.com/activities/",
		ActivitySource: "eventbus",
		ActivityKey:    "event-key",
	}, httpClient)
	require.NoError(t, err)

	for _, clusterID := range []string{"", "  ", "infra-dev", "tidb-serverless", "tidb123"} {
		matched, err := client.HasRelatedActivity(
			context.Background(),
			"TiDBClusterFrequentlyRestart",
			clusterID,
			time.Unix(1700003600, 0),
		)
		require.NoError(t, err, clusterID)
		require.False(t, matched, clusterID)
	}
	require.Equal(t, int32(0), calls.Load())
}

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

	httpClient := newTestHTTPClient(func(_ *http.Request) *http.Response {
		return jsonResponse(http.StatusUnauthorized, "unauthorized")
	})

	client, err := New(Config{
		BaseURL: "https://clinic.example.com",
		Token:   "bad-token",
	}, httpClient)
	require.NoError(t, err)

	_, err = client.GetCluster(context.Background(), "123")
	require.ErrorContains(t, err, "HTTP 401")
}

type roundTripFunc func(*http.Request) (*http.Response, error)

func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
	return f(req)
}

func newTestHTTPClient(handler func(*http.Request) *http.Response) *http.Client {
	return &http.Client{
		Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
			return handler(req), nil
		}),
	}
}

func jsonResponse(status int, body string) *http.Response {
	return &http.Response{
		StatusCode: status,
		Status:     fmt.Sprintf("%d %s", status, http.StatusText(status)),
		Header:     http.Header{"Content-Type": []string{"application/json"}},
		Body:       io.NopCloser(strings.NewReader(body)),
	}
}
