package service

import (
	"errors"
	"io"
	"net/http"
	"strings"
	"testing"

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

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

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

type errReadCloser struct {
	err error
}

func (e errReadCloser) Read([]byte) (int, error) {
	return 0, e.err
}

func (e errReadCloser) Close() error {
	return nil
}

func Test_doTiDBHTTPRequest(t *testing.T) {
	doErr := errors.New("do request failed")
	readErr := errors.New("read body failed")

	tests := []struct {
		name           string
		client         *http.Client
		expectedStatus int
		wantErr        bool
		wantErrIs      error
		wantHTTPErr    *TiDBHTTPError
	}{
		{
			name: "success",
			client: &http.Client{
				Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
					return &http.Response{
						StatusCode: http.StatusOK,
						Status:     "200 OK",
						Body:       io.NopCloser(strings.NewReader("ok")),
					}, nil
				}),
			},
			expectedStatus: http.StatusOK,
			wantErr:        false,
		},
		{
			name: "request failure",
			client: &http.Client{
				Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
					return nil, doErr
				}),
			},
			expectedStatus: http.StatusOK,
			wantErr:        true,
			wantErrIs:      doErr,
		},
		{
			name: "unexpected status",
			client: &http.Client{
				Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
					return &http.Response{
						StatusCode: http.StatusForbidden,
						Status:     "403 Forbidden",
						Body:       io.NopCloser(strings.NewReader("access denied")),
					}, nil
				}),
			},
			expectedStatus: http.StatusNoContent,
			wantErr:        true,
			wantHTTPErr: &TiDBHTTPError{
				StatusCode: http.StatusForbidden,
				Status:     "403 Forbidden",
				Body:       "access denied",
			},
		},
		{
			name: "unexpected status and read response body failure",
			client: &http.Client{
				Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
					return &http.Response{
						StatusCode: http.StatusInternalServerError,
						Status:     "500 Internal Server Error",
						Body:       errReadCloser{err: readErr},
					}, nil
				}),
			},
			expectedStatus: http.StatusNoContent,
			wantErr:        true,
			wantErrIs:      readErr,
			wantHTTPErr: &TiDBHTTPError{
				StatusCode: http.StatusInternalServerError,
				Status:     "500 Internal Server Error",
				Body:       "failed to read response body: read body failed",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req, err := http.NewRequest(http.MethodGet, "https://example.test", http.NoBody)
			require.NoError(t, err)

			err = doTiDBHTTPRequest(tt.client, req, tt.expectedStatus)
			if tt.wantErr {
				require.Error(t, err)
				if tt.wantErrIs != nil {
					require.ErrorIs(t, err, tt.wantErrIs)
				}
				if tt.wantHTTPErr != nil {
					var httpErr *TiDBHTTPError
					require.ErrorAs(t, err, &httpErr)
					assert.Equal(t, tt.wantHTTPErr.StatusCode, httpErr.StatusCode)
					assert.Equal(t, tt.wantHTTPErr.Status, httpErr.Status)
					assert.Equal(t, tt.wantHTTPErr.Body, httpErr.Body)
				}
				return
			}
			require.NoError(t, err)
		})
	}
}

func Test_sqlAddrToHTTPBaseURL(t *testing.T) {
	tests := []struct {
		name    string
		sqlAddr string
		want    string
		wantErr bool
	}{
		{
			name:    "normal address",
			sqlAddr: "db-group-tidb.ns.svc:4000",
			want:    "https://db-group-tidb.ns.svc:10080",
		},
		{
			name:    "localhost with port",
			sqlAddr: "127.0.0.1:4000",
			want:    "https://127.0.0.1:10080",
		},
		{
			name:    "IPv6 address",
			sqlAddr: "[::1]:4000",
			want:    "https://[::1]:10080",
		},
		{
			name:    "missing port",
			sqlAddr: "db-group-tidb.ns.svc",
			wantErr: true,
		},
		{
			name:    "empty string",
			sqlAddr: "",
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := sqlAddrToHTTPBaseURL(tt.sqlAddr)
			if tt.wantErr {
				assert.Error(t, err)
				return
			}
			require.NoError(t, err)
			assert.Equal(t, tt.want, got)
		})
	}
}

func Test_buildTiDBInternalAPIURL(t *testing.T) {
	tests := []struct {
		name    string
		baseURL string
		path    string
		want    string
		wantErr bool
	}{
		{
			name:    "valid https base URL",
			baseURL: "https://db-group-tidb.ns.svc:10080",
			path:    tidbInternalAPIPathResetPassword("root"),
			want:    "https://db-group-tidb.ns.svc:10080/internal/v1/users/root/reset-password",
		},
		{
			name:    "valid https base URL with trailing slash",
			baseURL: "https://db-group-tidb.ns.svc:10080/",
			path:    tidbInternalAPIPathDeleteCloudAdmin,
			want:    "https://db-group-tidb.ns.svc:10080/internal/v1/users/cloud_admin",
		},
		{
			name:    "reject non-https scheme",
			baseURL: "http://db-group-tidb.ns.svc:10080",
			path:    tidbInternalAPIPathDeleteCloudAdmin,
			wantErr: true,
		},
		{
			name:    "reject missing host",
			baseURL: "https://:10080",
			path:    tidbInternalAPIPathDeleteCloudAdmin,
			wantErr: true,
		},
		{
			name:    "reject missing port",
			baseURL: "https://db-group-tidb.ns.svc",
			path:    tidbInternalAPIPathDeleteCloudAdmin,
			wantErr: true,
		},
		{
			name:    "reject base URL with path",
			baseURL: "https://db-group-tidb.ns.svc:10080/internal/v1",
			path:    tidbInternalAPIPathDeleteCloudAdmin,
			wantErr: true,
		},
		{
			name:    "reject base URL with query",
			baseURL: "https://db-group-tidb.ns.svc:10080?foo=bar",
			path:    tidbInternalAPIPathDeleteCloudAdmin,
			wantErr: true,
		},
		{
			name:    "reject invalid host label",
			baseURL: "https://db_group-tidb.ns.svc:10080",
			path:    tidbInternalAPIPathDeleteCloudAdmin,
			wantErr: true,
		},
		{
			name:    "reject invalid API path",
			baseURL: "https://db-group-tidb.ns.svc:10080",
			path:    "internal/v1/users/cloud_admin",
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := buildTiDBInternalAPIURL(tt.baseURL, tt.path)
			if tt.wantErr {
				require.Error(t, err)
				return
			}

			require.NoError(t, err)
			assert.Equal(t, tt.want, got)
		})
	}
}

func Test_calcNextOrdinalForComponent(t *testing.T) {
	type args struct {
		replicas     int32
		deletedSlots []int32
	}
	tests := []struct {
		name                string
		args                args
		wantCurrentOrdinals []int32
		wantNextOrdinal     int32
	}{
		{
			name: "deletedSlots is empty",
			args: args{
				replicas:     3,
				deletedSlots: nil,
			},
			wantCurrentOrdinals: []int32{0, 1, 2},
			wantNextOrdinal:     3,
		},
		{
			name: "deletedSlots is non-empty and the length is less than replicas",
			args: args{
				replicas:     3,
				deletedSlots: []int32{0, 2},
			},
			wantCurrentOrdinals: []int32{1, 3, 4},
			wantNextOrdinal:     5,
		},
		{
			name: "deletedSlots is non-empty and the length is more than replicas",
			args: args{
				replicas:     3,
				deletedSlots: []int32{0, 2, 3, 5},
			},
			wantCurrentOrdinals: []int32{1, 4, 6},
			wantNextOrdinal:     7,
		},
		{
			name: "all deletedSlots are greater than replicas",
			args: args{
				replicas:     3,
				deletedSlots: []int32{5, 7},
			},
			wantCurrentOrdinals: []int32{0, 1, 2},
			wantNextOrdinal:     3,
		}, {
			name: "all deletedSlots are greater than replicas and deletedSlots is continuous with currentOrdinals",
			args: args{
				replicas:     3,
				deletedSlots: []int32{3, 4},
			},
			wantCurrentOrdinals: []int32{0, 1, 2},
			wantNextOrdinal:     5,
		}, {
			name: "all deletedSlots are greater than replicas and deletedSlots is continuous with currentOrdinals and have a middle slot",
			args: args{
				replicas:     3,
				deletedSlots: []int32{3, 5},
			},
			wantCurrentOrdinals: []int32{0, 1, 2},
			wantNextOrdinal:     4,
		},
		{
			name: "replicas is zero",
			args: args{
				replicas:     0,
				deletedSlots: []int32{5, 7},
			},
			wantCurrentOrdinals: nil,
			wantNextOrdinal:     0,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			currentOrdinals, nextOrdinal := calcNextOrdinal(tt.args.replicas, tt.args.deletedSlots)
			assert.Equalf(t, tt.wantCurrentOrdinals, currentOrdinals, "currentOrdinals")
			assert.Equalf(t, tt.wantNextOrdinal, nextOrdinal, "nextOrdinal")
		})
	}
}
