package meilisearch

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

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

// Mock structures for testing
type mockResponse struct {
	Message string `json:"message"`
}

type mockJsonMarshaller struct {
	valid bool
	null  bool
	Foo   string `json:"foo"`
	Bar   string `json:"bar"`
}

// failingEncoder is used to simulate encoder failure
type failingEncoder struct{}

func (fe failingEncoder) Encode(r io.Reader) (*bytes.Buffer, error) {
	return nil, errors.New("dummy encoding failure")
}

// Implement Decode method to satisfy the encoder interface, though it won't be used here
func (fe failingEncoder) Decode(b []byte, v interface{}) error {
	return errors.New("dummy decode failure")
}

func TestExecuteRequest(t *testing.T) {
	retryCount := 0

	// Create a mock server
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodGet && r.URL.Path == "/test-get" {
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write([]byte(`{"message":"get successful"}`))
		} else if r.Method == http.MethodGet && r.URL.Path == "/test-get-encoding" {
			encode := r.Header.Get("Accept-Encoding")
			if len(encode) != 0 {
				enc := newEncoding(ContentEncoding(encode), DefaultCompression)
				d := &mockData{Name: "foo", Age: 30}

				b, err := json.Marshal(d)
				require.NoError(t, err)

				res, err := enc.Encode(bytes.NewReader(b))
				require.NoError(t, err)
				_, _ = w.Write(res.Bytes())
				w.WriteHeader(http.StatusOK)
				return
			}
			_, _ = w.Write([]byte("invalid message"))
			w.WriteHeader(http.StatusInternalServerError)
		} else if r.Method == http.MethodPost && r.URL.Path == "/test-req-resp-encoding" {
			accept := r.Header.Get("Accept-Encoding")
			ce := r.Header.Get("Content-Encoding")

			reqEnc := newEncoding(ContentEncoding(ce), DefaultCompression)
			respEnc := newEncoding(ContentEncoding(accept), DefaultCompression)
			req := new(mockData)

			if len(ce) != 0 {
				b, err := io.ReadAll(r.Body)
				require.NoError(t, err)

				err = reqEnc.Decode(b, req)
				require.NoError(t, err)
			}

			if len(accept) != 0 {
				d, err := json.Marshal(req)
				require.NoError(t, err)
				res, err := respEnc.Encode(bytes.NewReader(d))
				require.NoError(t, err)
				_, _ = w.Write(res.Bytes())
				w.WriteHeader(http.StatusOK)
			}
		} else if r.Method == http.MethodPost && r.URL.Path == "/test-post" {
			w.WriteHeader(http.StatusCreated)
			msg := []byte(`{"message":"post successful"}`)
			_, _ = w.Write(msg)

		} else if r.Method == http.MethodGet && r.URL.Path == "/test-null-body" {
			w.WriteHeader(http.StatusOK)
			msg := []byte(`null`)
			_, _ = w.Write(msg)
		} else if r.Method == http.MethodPost && r.URL.Path == "/test-post-encoding" {
			w.WriteHeader(http.StatusCreated)
			msg := []byte(`{"message":"post successful"}`)

			enc := r.Header.Get("Accept-Encoding")
			if len(enc) != 0 {
				e := newEncoding(ContentEncoding(enc), DefaultCompression)
				b, err := e.Encode(bytes.NewReader(msg))
				require.NoError(t, err)
				_, _ = w.Write(b.Bytes())
				return
			}
			_, _ = w.Write(msg)
		} else if r.URL.Path == "/test-bad-request" {
			w.WriteHeader(http.StatusBadRequest)
			_, _ = w.Write([]byte(`{"message":"bad request"}`))
		} else if r.URL.Path == "/invalid-response-body" {
			w.WriteHeader(http.StatusInternalServerError)
			_, _ = w.Write([]byte(`{"message":"bad response body"}`))
		} else if r.URL.Path == "/io-reader" {
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write([]byte(`{"message":"io reader"}`))
		} else if r.URL.Path == "/failed-retry" {
			w.WriteHeader(http.StatusBadGateway)
		} else if r.URL.Path == "/success-retry" {
			if retryCount == 2 {
				w.WriteHeader(http.StatusOK)
				return
			}
			w.WriteHeader(http.StatusBadGateway)
			retryCount++
		} else if r.URL.Path == "/dummy" {
			w.WriteHeader(http.StatusOK)
		} else {
			w.WriteHeader(http.StatusNotFound)
		}
	}))
	defer ts.Close()

	tests := []struct {
		name            string
		internalReq     *internalRequest
		expectedResp    interface{}
		contentEncoding ContentEncoding
		withTimeout     bool
		disableRetry    bool
		wantErr         bool
	}{
		{
			name: "Successful GET request",
			internalReq: &internalRequest{
				endpoint:            "/test-get",
				method:              http.MethodGet,
				withResponse:        &mockResponse{},
				acceptedStatusCodes: []int{http.StatusOK},
			},
			expectedResp: &mockResponse{Message: "get successful"},
			wantErr:      false,
		},
		{
			name: "Successful POST request",
			internalReq: &internalRequest{
				endpoint:            "/test-post",
				method:              http.MethodPost,
				withRequest:         map[string]string{"key": "value"},
				contentType:         contentTypeJSON,
				withResponse:        &mockResponse{},
				acceptedStatusCodes: []int{http.StatusCreated},
			},
			expectedResp: &mockResponse{Message: "post successful"},
			wantErr:      false,
		},
		{
			name: "404 Not Found",
			internalReq: &internalRequest{
				endpoint:            "/not-found",
				method:              http.MethodGet,
				withResponse:        &mockResponse{},
				acceptedStatusCodes: []int{http.StatusOK},
			},
			expectedResp: nil,
			wantErr:      true,
		},
		{
			name: "Invalid URL",
			internalReq: &internalRequest{
				endpoint:            "/invalid-url$%^*()*#",
				method:              http.MethodGet,
				withResponse:        &mockResponse{},
				acceptedStatusCodes: []int{http.StatusOK},
			},
			expectedResp: nil,
			wantErr:      true,
		},
		{
			name: "Invalid response body",
			internalReq: &internalRequest{
				endpoint:            "/invalid-response-body",
				method:              http.MethodGet,
				withResponse:        struct{}{},
				acceptedStatusCodes: []int{http.StatusInternalServerError},
			},
			expectedResp: nil,
			wantErr:      true,
		},
		{
			name: "Invalid request method",
			internalReq: &internalRequest{
				endpoint:            "/invalid-request-method",
				method:              http.MethodGet,
				withResponse:        nil,
				withRequest:         struct{}{},
				acceptedStatusCodes: []int{http.StatusBadRequest},
			},
			expectedResp: nil,
			wantErr:      true,
		},
		{
			name: "Invalid request content type",
			internalReq: &internalRequest{
				endpoint:            "/invalid-request-content-type",
				method:              http.MethodPost,
				withResponse:        nil,
				contentType:         "",
				withRequest:         struct{}{},
				acceptedStatusCodes: []int{http.StatusBadRequest},
			},
			expectedResp: nil,
			wantErr:      true,
		},
		{
			name: "Invalid json marshaler",
			internalReq: &internalRequest{
				endpoint:     "/invalid-marshaler",
				method:       http.MethodPost,
				withResponse: nil,
				withRequest: &mockJsonMarshaller{
					valid: false,
				},
				contentType: "application/json",
			},
			expectedResp: nil,
			wantErr:      true,
		},
		{
			name: "Null data marshaler",
			internalReq: &internalRequest{
				endpoint:     "/null-data-marshaler",
				method:       http.MethodPost,
				withResponse: nil,
				withRequest: &mockJsonMarshaller{
					valid: true,
					null:  true,
				},
				contentType: "application/json",
			},
			expectedResp: nil,
			wantErr:      true,
		},
		{
			name: "Test null body response",
			internalReq: &internalRequest{
				endpoint:            "/test-null-body",
				method:              http.MethodGet,
				withResponse:        make([]byte, 0),
				contentType:         "application/json",
				acceptedStatusCodes: []int{http.StatusOK},
			},
			expectedResp: nil,
			wantErr:      false,
		},
		{
			name: "400 Bad Request",
			internalReq: &internalRequest{
				endpoint:            "/test-bad-request",
				method:              http.MethodGet,
				withResponse:        &mockResponse{},
				acceptedStatusCodes: []int{http.StatusOK},
			},
			expectedResp: nil,
			wantErr:      true,
		},
		{
			name: "Test request encoding gzip",
			internalReq: &internalRequest{
				endpoint:            "/test-post-encoding",
				method:              http.MethodPost,
				withRequest:         map[string]string{"key": "value"},
				contentType:         contentTypeJSON,
				withResponse:        &mockResponse{},
				acceptedStatusCodes: []int{http.StatusCreated},
			},
			expectedResp:    &mockResponse{Message: "post successful"},
			contentEncoding: GzipEncoding,
			wantErr:         false,
		},
		{
			name: "Test request encoding deflate",
			internalReq: &internalRequest{
				endpoint:            "/test-post-encoding",
				method:              http.MethodPost,
				withRequest:         map[string]string{"key": "value"},
				contentType:         contentTypeJSON,
				withResponse:        &mockResponse{},
				acceptedStatusCodes: []int{http.StatusCreated},
			},
			expectedResp:    &mockResponse{Message: "post successful"},
			contentEncoding: DeflateEncoding,
			wantErr:         false,
		},
		{
			name: "Test request encoding brotli",
			internalReq: &internalRequest{
				endpoint:            "/test-post-encoding",
				method:              http.MethodPost,
				withRequest:         map[string]string{"key": "value"},
				contentType:         contentTypeJSON,
				withResponse:        &mockResponse{},
				acceptedStatusCodes: []int{http.StatusCreated},
			},
			expectedResp:    &mockResponse{Message: "post successful"},
			contentEncoding: BrotliEncoding,
			wantErr:         false,
		},
		{
			name: "Test response decoding gzip",
			internalReq: &internalRequest{
				endpoint:            "/test-get-encoding",
				method:              http.MethodGet,
				withRequest:         nil,
				withResponse:        &mockData{},
				acceptedStatusCodes: []int{http.StatusOK},
			},
			expectedResp:    &mockData{Name: "foo", Age: 30},
			contentEncoding: GzipEncoding,
			wantErr:         false,
		},
		{
			name: "Test response decoding deflate",
			internalReq: &internalRequest{
				endpoint:            "/test-get-encoding",
				method:              http.MethodGet,
				withRequest:         nil,
				withResponse:        &mockData{},
				acceptedStatusCodes: []int{http.StatusOK},
			},
			expectedResp:    &mockData{Name: "foo", Age: 30},
			contentEncoding: DeflateEncoding,
			wantErr:         false,
		},
		{
			name: "Test response decoding brotli",
			internalReq: &internalRequest{
				endpoint:            "/test-get-encoding",
				method:              http.MethodGet,
				withRequest:         nil,
				withResponse:        &mockData{},
				acceptedStatusCodes: []int{http.StatusOK},
			},
			expectedResp:    &mockData{Name: "foo", Age: 30},
			contentEncoding: BrotliEncoding,
			wantErr:         false,
		},
		{
			name: "Test request and response encoding",
			internalReq: &internalRequest{
				endpoint:            "/test-req-resp-encoding",
				method:              http.MethodPost,
				contentType:         contentTypeJSON,
				withRequest:         &mockData{Name: "foo", Age: 30},
				withResponse:        &mockData{},
				acceptedStatusCodes: []int{http.StatusOK},
			},
			expectedResp:    &mockData{Name: "foo", Age: 30},
			contentEncoding: GzipEncoding,
			wantErr:         false,
		},
		{
			name: "Test successful retries",
			internalReq: &internalRequest{
				endpoint:            "/success-retry",
				method:              http.MethodGet,
				withResponse:        nil,
				withRequest:         nil,
				acceptedStatusCodes: []int{http.StatusOK},
			},
			expectedResp: nil,
			wantErr:      false,
		},
		{
			name: "Test failed retries",
			internalReq: &internalRequest{
				endpoint:            "/failed-retry",
				method:              http.MethodGet,
				withResponse:        nil,
				withRequest:         nil,
				acceptedStatusCodes: []int{http.StatusOK},
			},
			expectedResp: nil,
			wantErr:      true,
		},
		{
			name: "Test disable retries",
			internalReq: &internalRequest{
				endpoint:            "/test-get",
				method:              http.MethodGet,
				withResponse:        nil,
				withRequest:         nil,
				acceptedStatusCodes: []int{http.StatusOK},
			},
			expectedResp: nil,
			disableRetry: true,
			wantErr:      false,
		},
		{
			name: "Test request timeout on retries",
			internalReq: &internalRequest{
				endpoint:            "/failed-retry",
				method:              http.MethodGet,
				withResponse:        nil,
				withRequest:         nil,
				acceptedStatusCodes: []int{http.StatusOK},
			},
			expectedResp: nil,
			withTimeout:  true,
			wantErr:      true,
		},
		{
			name: "Test request encoding with []byte encode failure",
			internalReq: &internalRequest{
				endpoint:            "/dummy",
				method:              http.MethodPost,
				withRequest:         []byte("test data"),
				contentType:         "text/plain",
				acceptedStatusCodes: []int{http.StatusOK},
			},
			expectedResp:    nil,
			contentEncoding: GzipEncoding,
			wantErr:         true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := newClient(&http.Client{}, ts.URL, "testApiKey", clientConfig{
				contentEncoding:          tt.contentEncoding,
				encodingCompressionLevel: DefaultCompression,
				maxRetries:               3,
				disableRetry:             tt.disableRetry,
				retryOnStatus: map[int]bool{
					502: true,
					503: true,
					504: true,
				},
			})

			// For the specific test case, override the encoder to force an error
			if tt.name == "Test request encoding with []byte encode failure" {
				c.encoder = failingEncoder{}
			}

			ctx := context.Background()

			if tt.withTimeout {
				timeoutCtx, cancel := context.WithTimeout(ctx, 1*time.Second)
				ctx = timeoutCtx
				defer cancel()
			}

			err := c.executeRequest(ctx, tt.internalReq)
			if tt.wantErr {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
				require.Equal(t, tt.expectedResp, tt.internalReq.withResponse)
			}
		})
	}
}

func TestNewClientNilRetryOnStatus(t *testing.T) {
	c := newClient(&http.Client{}, "", "", clientConfig{
		maxRetries:    3,
		retryOnStatus: nil,
	})

	require.NotNil(t, c.retryOnStatus)
}

func (m mockJsonMarshaller) MarshalJSON() ([]byte, error) {
	type Alias mockJsonMarshaller

	if !m.valid {
		return nil, errors.New("mockJsonMarshaller not valid")
	}

	if m.null {
		return nil, nil
	}

	return json.Marshal(&struct {
		Alias
	}{
		Alias: Alias(m),
	})
}
