package clusteragent

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

	"github.com/go-resty/resty/v2"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/ticdc-service-ng/pkg/util"
)

func newTestClient(t *testing.T, server *httptest.Server, namespace string) Client {
	t.Helper()
	cli := resty.New().SetBaseURL(server.URL)
	return &clientImpl{
		cli:       cli,
		namespace: namespace,
	}
}

func TestClientGetPublicKey(t *testing.T) {
	tests := []struct {
		name      string
		namespace string
		handler   http.HandlerFunc
		wantErr   bool
		wantPEM   []byte
		wantCode  int
	}{
		{
			name:      "successful get public key",
			namespace: "tidb123",
			handler: func(w http.ResponseWriter, r *http.Request) {
				assert.Equal(t, "/cluster-agent/tidb123/api/v1/public_key", r.URL.EscapedPath())
				assert.Equal(t, http.MethodGet, r.Method)
				resp := getPublicKeyResp{Pem: []byte("test-pem-data")}
				w.Header().Set("Content-Type", "application/json")
				json.NewEncoder(w).Encode(resp)
			},
			wantPEM: []byte("test-pem-data"),
		},
		{
			name:      "namespace is escaped",
			namespace: "tidb/123",
			handler: func(w http.ResponseWriter, r *http.Request) {
				assert.Equal(t, "/cluster-agent/tidb%2F123/api/v1/public_key", r.URL.EscapedPath())
				assert.Equal(t, http.MethodGet, r.Method)
				resp := getPublicKeyResp{Pem: []byte("test-pem-data")}
				w.Header().Set("Content-Type", "application/json")
				json.NewEncoder(w).Encode(resp)
			},
			wantPEM: []byte("test-pem-data"),
		},
		{
			name:      "server returns error",
			namespace: "tidb123",
			handler: func(w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(http.StatusInternalServerError)
				w.Write([]byte("internal error"))
			},
			wantErr:  true,
			wantCode: http.StatusInternalServerError,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := httptest.NewServer(tt.handler)
			defer server.Close()

			client := newTestClient(t, server, tt.namespace)
			pem, err := client.GetPublicKey(context.Background())
			if tt.wantErr {
				assert.Error(t, err)
				var statusErr *util.StatusError
				require.True(t, errors.As(err, &statusErr))
				assert.Equal(t, tt.wantCode, statusErr.StatusCode)
				return
			}
			require.NoError(t, err)
			assert.Equal(t, tt.wantPEM, pem)
		})
	}
}

func TestClientQueryTiCDCTablesEligibility(t *testing.T) {
	tests := []struct {
		name                  string
		namespace             string
		clusterID             string
		handler               http.HandlerFunc
		wantErr               bool
		wantEligibleSchemas   []string
		wantEligibleTables    []string
		wantIneligibleSchemas []string
		wantIneligibleTables  []string
		wantCode              int
	}{
		{
			name:      "returns both eligible and ineligible tables",
			namespace: "tidb456",
			clusterID: "cluster-1",
			handler: func(w http.ResponseWriter, r *http.Request) {
				assert.Equal(t, "/cluster-agent/tidb456/api/v1/clusters/cluster-1:query_ticdc_tables_eligibility", r.URL.EscapedPath())
				assert.Equal(t, http.MethodPost, r.Method)

				var body queryTablesEligibilityReq
				require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
				assert.Equal(t, []byte("enc-user"), body.EncryptedUsername)
				assert.Equal(t, []byte("enc-pwd"), body.EncryptedPassword)

				resp := queryTablesEligibilityResp{
					EligibleTables: tableSchemaInfo{
						TableSchemas: []string{"db1", "db2"},
						TableNames:   []string{"t1", "t2"},
					},
					IneligibleTables: tableSchemaInfo{
						TableSchemas: []string{"db3"},
						TableNames:   []string{"t3"},
					},
				}
				w.Header().Set("Content-Type", "application/json")
				json.NewEncoder(w).Encode(resp)
			},
			wantEligibleSchemas:   []string{"db1", "db2"},
			wantEligibleTables:    []string{"t1", "t2"},
			wantIneligibleSchemas: []string{"db3"},
			wantIneligibleTables:  []string{"t3"},
		},
		{
			name:      "cluster id is escaped",
			namespace: "tidb456",
			clusterID: "cluster/1",
			handler: func(w http.ResponseWriter, r *http.Request) {
				assert.Equal(t, "/cluster-agent/tidb456/api/v1/clusters/cluster%2F1:query_ticdc_tables_eligibility", r.URL.EscapedPath())
				assert.Equal(t, http.MethodPost, r.Method)

				resp := queryTablesEligibilityResp{
					EligibleTables: tableSchemaInfo{
						TableSchemas: []string{"db1"},
						TableNames:   []string{"t1"},
					},
				}
				w.Header().Set("Content-Type", "application/json")
				json.NewEncoder(w).Encode(resp)
			},
			wantEligibleSchemas: []string{"db1"},
			wantEligibleTables:  []string{"t1"},
		},
		{
			name:      "namespace is escaped",
			namespace: "tidb/456",
			clusterID: "cluster-1",
			handler: func(w http.ResponseWriter, r *http.Request) {
				assert.Equal(t, "/cluster-agent/tidb%2F456/api/v1/clusters/cluster-1:query_ticdc_tables_eligibility", r.URL.EscapedPath())
				assert.Equal(t, http.MethodPost, r.Method)

				resp := queryTablesEligibilityResp{
					EligibleTables: tableSchemaInfo{
						TableSchemas: []string{"db1"},
						TableNames:   []string{"t1"},
					},
				}
				w.Header().Set("Content-Type", "application/json")
				json.NewEncoder(w).Encode(resp)
			},
			wantEligibleSchemas: []string{"db1"},
			wantEligibleTables:  []string{"t1"},
		},
		{
			name:      "server error",
			namespace: "tidb456",
			clusterID: "cluster-1",
			handler: func(w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(http.StatusBadRequest)
				w.Write([]byte("bad request"))
			},
			wantErr:  true,
			wantCode: http.StatusBadRequest,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := httptest.NewServer(tt.handler)
			defer server.Close()

			client := newTestClient(t, server, tt.namespace)
			eligible, ineligible, err := client.QueryTiCDCTablesEligibility(
				context.Background(), tt.clusterID,
				[]byte("enc-user"), []byte("enc-pwd"),
			)
			if tt.wantErr {
				assert.Error(t, err)
				var statusErr *util.StatusError
				require.True(t, errors.As(err, &statusErr))
				assert.Equal(t, tt.wantCode, statusErr.StatusCode)
				return
			}
			require.NoError(t, err)
			assert.Equal(t, tt.wantEligibleSchemas, eligible.Schemas)
			assert.Equal(t, tt.wantEligibleTables, eligible.Tables)
			assert.Equal(t, tt.wantIneligibleSchemas, ineligible.Schemas)
			assert.Equal(t, tt.wantIneligibleTables, ineligible.Tables)
		})
	}
}

func TestClientGetPublicKeyTruncatesErrorBody(t *testing.T) {
	const maxBodySize = util.MaxBodySize
	largeBody := make([]byte, maxBodySize+128)
	for i := range largeBody {
		largeBody[i] = 'x'
	}

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusInternalServerError)
		_, _ = w.Write(largeBody)
	}))
	defer server.Close()

	client := newTestClient(t, server, "tidb123")
	_, err := client.GetPublicKey(context.Background())
	require.Error(t, err)

	var statusErr *util.StatusError
	require.True(t, errors.As(err, &statusErr))
	assert.Equal(t, http.StatusInternalServerError, statusErr.StatusCode)
	assert.Equal(t, maxBodySize, len(statusErr.Body))
}

func TestClientQueryTiCDCTablesEligibilityTruncatesErrorBody(t *testing.T) {
	const maxBodySize = util.MaxBodySize
	largeBody := make([]byte, maxBodySize+128)
	for i := range largeBody {
		largeBody[i] = 'x'
	}

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusBadRequest)
		_, _ = w.Write(largeBody)
	}))
	defer server.Close()

	client := newTestClient(t, server, "tidb123")
	_, _, err := client.QueryTiCDCTablesEligibility(
		context.Background(), "cluster-1",
		[]byte("enc-user"), []byte("enc-pwd"),
	)
	require.Error(t, err)

	var statusErr *util.StatusError
	require.True(t, errors.As(err, &statusErr))
	assert.Equal(t, http.StatusBadRequest, statusErr.StatusCode)
	assert.Equal(t, maxBodySize, len(statusErr.Body))
}
