package server

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"

	"github.com/pingcap/kvproto/pkg/keyspacepb"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/aws-shared-provider/pkg/metaserver/storage"
)

func TestRegisterHandlersExposesMetrics(t *testing.T) {
	server := &Server{
		mux: http.NewServeMux(),
		store: &fakeStorage{
			eventWindow: storage.ClusterEventWindow{
				Epoch:  "test-epoch",
				Events: []storage.ClusterEvent{},
			},
		},
	}
	registerHandlers(server)

	request := httptest.NewRequest(http.MethodGet, "/metrics", nil)
	response := httptest.NewRecorder()
	server.mux.ServeHTTP(response, request)

	require.Equal(t, http.StatusOK, response.Code)
	require.Contains(t, response.Body.String(), "metaserver_cluster_cache_entries")
	require.Contains(t, response.Body.String(), "metaserver_mysql_configured")
	require.Contains(t, response.Body.String(), "metaserver_mysql_last_operation_success")
	require.Contains(t, response.Body.String(), "go_goroutines")

	request = httptest.NewRequest(http.MethodGet, "/api/v1/events", nil)
	response = httptest.NewRecorder()
	server.mux.ServeHTTP(response, request)

	require.Equal(t, http.StatusOK, response.Code)
	require.JSONEq(t,
		`{"epoch":"test-epoch","high_watermark":0,"rewind_lower_bound":0,"events":[]}`,
		response.Body.String(),
	)

	request = httptest.NewRequest(http.MethodPost, "/api/v1/cache/validate", nil)
	response = httptest.NewRecorder()
	server.mux.ServeHTTP(response, request)

	require.Equal(t, http.StatusNotFound, response.Code)
}

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

	filter, err := parseKeyspaceListFilter(url.Values{
		"state": []string{"ARCHIVED"},
	})
	require.NoError(t, err)
	require.True(t, filter.HasState)
	require.Equal(t, keyspacepb.KeyspaceState_ARCHIVED, filter.State)
}

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

	state, err := parseKeyspaceState("2")
	require.NoError(t, err)
	require.Equal(t, keyspacepb.KeyspaceState_ARCHIVED, state)
}

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

	store := &fakeStorage{
		keyspaces: []*storage.KeyspaceMetaData{
			{
				KeyspaceID:   "1",
				KeyspaceName: "ks-1",
				State:        keyspacepb.KeyspaceState_ARCHIVED,
			},
		},
	}
	server := &Server{store: store}
	req := httptest.NewRequest(http.MethodGet, "/api/v2/meta:list?state=ARCHIVED", nil)
	recorder := httptest.NewRecorder()

	server.handleListKeyspaceMetaV2(recorder, req)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.False(t, store.paginatedCalled)
	var items []*storage.KeyspaceMetaData
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &items))
	require.Len(t, items, 1)
	require.Equal(t, "ks-1", items[0].KeyspaceName)
}

func TestHandleClusterEventList(t *testing.T) {
	store := &fakeStorage{
		eventWindow: storage.ClusterEventWindow{
			Epoch:            "test-epoch",
			HighWatermark:    12,
			RewindLowerBound: 9,
			Events: []storage.ClusterEvent{
				{
					EventID:         10,
					Name:            "deleted",
					EventType:       "CLUSTER_DELETED",
					ResourceVersion: 4,
				},
				{
					EventID:         12,
					Name:            "updated",
					EventType:       "CLUSTER_UPDATED",
					ResourceVersion: 5,
				},
			},
		},
	}
	server := &Server{store: store}
	req := httptest.NewRequest(http.MethodGet, "/api/v1/events", nil)
	recorder := httptest.NewRecorder()

	server.handleClusterEventList(recorder, req)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.Equal(t, "application/json", recorder.Header().Get("Content-Type"))
	require.JSONEq(t, `{
		"epoch": "test-epoch",
		"high_watermark": 12,
		"rewind_lower_bound": 9,
		"events": [
			{"event_id": 10, "name": "deleted", "event_type": "CLUSTER_DELETED", "resource_version": 4},
			{"event_id": 12, "name": "updated", "event_type": "CLUSTER_UPDATED", "resource_version": 5}
		]
	}`, recorder.Body.String())
}

func TestHandleClusterEventListRejectsInvalidMethodAndStorageFailure(t *testing.T) {
	tests := []struct {
		name   string
		method string
		err    error
		status int
	}{
		{name: "method", method: http.MethodPost, status: http.StatusMethodNotAllowed},
		{name: "storage", method: http.MethodGet, err: errors.New("unavailable"), status: http.StatusInternalServerError},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := &Server{store: &fakeStorage{eventWindowErr: tt.err}}
			req := httptest.NewRequest(tt.method, "/api/v1/events", nil)
			recorder := httptest.NewRecorder()

			server.handleClusterEventList(recorder, req)

			require.Equal(t, tt.status, recorder.Code)
		})
	}
}

type fakeStorage struct {
	storage.Storage
	keyspaces        []*storage.KeyspaceMetaData
	eventWindow      storage.ClusterEventWindow
	eventWindowErr   error
	eventWindowCalls int
	paginatedCalled  bool
}

func (f *fakeStorage) ListKeyspaces(filter storage.KeyspaceListFilter) ([]*storage.KeyspaceMetaData, error) {
	return f.keyspaces, nil
}

func (f *fakeStorage) ListKeyspacesPaginated(filter storage.KeyspaceListFilter, limit, offset int) ([]*storage.KeyspaceMetaData, int, error) {
	f.paginatedCalled = true
	return f.keyspaces, len(f.keyspaces), nil
}

func (f *fakeStorage) GetClusterEventWindow() (storage.ClusterEventWindow, error) {
	f.eventWindowCalls++
	return f.eventWindow, f.eventWindowErr
}
