package interceptor

import (
	"bufio"
	"context"
	"net"
	"net/http"
	"net/url"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
	"github.com/tidbcloud/observability/pkg/common/constants"
	"github.com/tidbcloud/observability/pkg/common/structs"
	"go.uber.org/zap"
)

type fakeResponseWriter struct {
	status int
}

var _ gin.ResponseWriter = &fakeResponseWriter{}

func (w *fakeResponseWriter) Header() http.Header {
	return http.Header{}
}

func (w *fakeResponseWriter) WriteHeader(code int) {
}

func (w *fakeResponseWriter) WriteHeaderNow() {
}

func (w *fakeResponseWriter) Write(data []byte) (n int, err error) {
	return 0, nil
}

func (w *fakeResponseWriter) WriteString(s string) (n int, err error) {
	return 0, nil
}

func (w *fakeResponseWriter) Status() int {
	return w.status
}
func (w *fakeResponseWriter) Size() int {
	return 0
}

func (w *fakeResponseWriter) Written() bool {
	return true
}

// Hijack implements the http.Hijacker interface.
func (w *fakeResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
	return nil, nil, nil
}

// CloseNotify implements the http.CloseNotify interface.
func (w *fakeResponseWriter) CloseNotify() <-chan bool {
	n := make(chan bool)
	return n
}

// Flush implements the http.Flush interface.
func (w *fakeResponseWriter) Flush() {
}

func (w *fakeResponseWriter) Pusher() (pusher http.Pusher) {
	return nil
}

func Test_Log(t *testing.T) {
	t.Run("gin context is nil", func(t *testing.T) {
		Log(nil)
	})

	t.Run("gin context with nil writer", func(t *testing.T) {
		c := &gin.Context{}
		Log(c)
	})

	t.Run("gin context with nil request", func(t *testing.T) {
		c := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusOK},
		}
		Log(c)
	})

	t.Run("http status code not 200", func(t *testing.T) {
		c := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusInternalServerError},
		}
		Log(c)
	})

	t.Run("http status code 200", func(t *testing.T) {
		c := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusOK},
		}
		Log(c)
	})

	t.Run("request id is nil", func(t *testing.T) {
		c := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusOK},
		}
		Log(c)
		assert.True(t, c.Keys[string(constants.OpenAPIHeaderRequestId)] != "")
	})

	t.Run("request id from header", func(t *testing.T) {
		test_url := url.URL{
			Path: "/test",
		}
		existingRequestID := "test-request-id-123"
		c := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusOK},
			Request: &http.Request{
				Header: make(http.Header),
				URL:    &test_url,
			},
		}
		c.Request.Header.Set(string(constants.OpenAPIHeaderRequestId), existingRequestID)
		Log(c)
		assert.Equal(t, existingRequestID, c.Keys[string(constants.OpenAPIHeaderRequestId)])
	})

	t.Run("different request with different request id", func(t *testing.T) {
		test_url := url.URL{
			Path: "path",
		}
		c1 := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusOK},
			Request: &http.Request{
				Header: make(http.Header),
				URL:    &test_url,
			},
		}
		c2 := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusOK},
			Request: &http.Request{
				Header: make(http.Header),
				URL:    &test_url,
			},
		}
		Log(c1)
		Log(c2)
		assert.True(t, c1.Keys[string(constants.OpenAPIHeaderRequestId)] != c2.Keys[string(constants.OpenAPIHeaderRequestId)])
	})

	t.Run("with global server request parameters", func(t *testing.T) {
		test_url := url.URL{
			Path: "/api/test",
		}
		c := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusOK},
			Request: &http.Request{
				Header: make(http.Header),
				URL:    &test_url,
				Method: http.MethodPost,
			},
		}

		reqParams := structs.RequestParameters{
			CommonReqInfo: structs.CommonReqInfo{
				Vendor: constants.AWS,
				Region: "us-west-2",
			},
			Headers:     make(map[string]string),
			QueryParams: make(map[string]string),
			PathParams:  []gin.Param{},
			FormParams:  make(map[string]string),
			BodyParams:  "",
		}
		c.Set(structs.GlobalServerRequestParametersKey, reqParams)

		Log(c)
		assert.True(t, c.Keys[string(constants.OpenAPIHeaderRequestId)] != "")
	})

	t.Run("http status code 300", func(t *testing.T) {
		c := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusMultipleChoices},
		}
		Log(c)
	})

	t.Run("http status code 400", func(t *testing.T) {
		c := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusBadRequest},
		}
		Log(c)
	})

	t.Run("http status code 404", func(t *testing.T) {
		test_url := url.URL{
			Path: "/not-found",
		}
		c := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusNotFound},
			Request: &http.Request{
				Header: make(http.Header),
				URL:    &test_url,
				Method: http.MethodGet,
			},
		}
		Log(c)
	})

	t.Run("http status code 100", func(t *testing.T) {
		test_url := url.URL{
			Path: "/continue",
		}
		c := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusContinue},
			Request: &http.Request{
				Header: make(http.Header),
				URL:    &test_url,
				Method: http.MethodGet,
			},
		}
		Log(c)
	})

	t.Run("http status code 500", func(t *testing.T) {
		c := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusInternalServerError},
		}
		Log(c)
	})

	t.Run("http status code 502", func(t *testing.T) {
		test_url := url.URL{
			Path: "/gateway-error",
		}
		c := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusBadGateway},
			Request: &http.Request{
				Header: make(http.Header),
				URL:    &test_url,
				Method: http.MethodPost,
			},
		}
		Log(c)
	})

	t.Run("empty request id header", func(t *testing.T) {
		test_url := url.URL{
			Path: "/test-empty-header",
		}
		c := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusOK},
			Request: &http.Request{
				Header: make(http.Header),
				URL:    &test_url,
				Method: http.MethodGet,
			},
		}
		c.Request.Header.Set(string(constants.OpenAPIHeaderRequestId), "")
		Log(c)
		// Should generate a new UUID since header value is empty
		assert.True(t, c.Keys[string(constants.OpenAPIHeaderRequestId)] != "")
	})

	t.Run("request with method and url", func(t *testing.T) {
		test_url := url.URL{
			Path: "/api/v1/test",
		}
		c := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusOK},
			Request: &http.Request{
				Header: make(http.Header),
				URL:    &test_url,
				Method: http.MethodPost,
			},
		}
		Log(c)
		assert.True(t, c.Keys[string(constants.OpenAPIHeaderRequestId)] != "")
	})

	t.Run("request with global server parameters and different vendor", func(t *testing.T) {
		test_url := url.URL{
			Path: "/api/test-gcp",
		}
		c := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusOK},
			Request: &http.Request{
				Header: make(http.Header),
				URL:    &test_url,
				Method: http.MethodPost,
			},
		}

		// Test with GCP vendor
		reqParams := structs.RequestParameters{
			CommonReqInfo: structs.CommonReqInfo{
				Vendor: constants.GCP,
				Region: "us-central1",
			},
			Headers:     make(map[string]string),
			QueryParams: make(map[string]string),
			PathParams:  []gin.Param{},
			FormParams:  make(map[string]string),
			BodyParams:  "",
		}
		c.Set(structs.GlobalServerRequestParametersKey, reqParams)

		Log(c)
		assert.True(t, c.Keys[string(constants.OpenAPIHeaderRequestId)] != "")
	})

	t.Run("request with path params in global server parameters", func(t *testing.T) {
		test_url := url.URL{
			Path: "/api/test/:id",
		}
		c := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusOK},
			Request: &http.Request{
				Header: make(http.Header),
				URL:    &test_url,
				Method: http.MethodGet,
			},
		}

		// Test with path parameters
		reqParams := structs.RequestParameters{
			CommonReqInfo: structs.CommonReqInfo{
				Vendor: constants.Azure,
				Region: "eastus",
			},
			Headers:     map[string]string{"Content-Type": "application/json"},
			QueryParams: map[string]string{"limit": "10"},
			PathParams:  []gin.Param{{Key: "id", Value: "123"}},
			FormParams:  map[string]string{"form_field": "value"},
			BodyParams:  `{"test": "data"}`,
		}
		c.Set(structs.GlobalServerRequestParametersKey, reqParams)

		Log(c)
		assert.True(t, c.Keys[string(constants.OpenAPIHeaderRequestId)] != "")
	})

	t.Run("status code edge case 199", func(t *testing.T) {
		test_url := url.URL{
			Path: "/status-199",
		}
		c := &gin.Context{
			Writer: &fakeResponseWriter{199}, // Edge case: just below 200
			Request: &http.Request{
				Header: make(http.Header),
				URL:    &test_url,
				Method: http.MethodGet,
			},
		}
		Log(c)
	})

	t.Run("status code 299", func(t *testing.T) {
		test_url := url.URL{
			Path: "/status-299",
		}
		c := &gin.Context{
			Writer: &fakeResponseWriter{299}, // Edge case: at upper bound of 2xx
			Request: &http.Request{
				Header: make(http.Header),
				URL:    &test_url,
				Method: http.MethodPost,
			},
		}
		Log(c)
	})

	t.Run("status code 399", func(t *testing.T) {
		test_url := url.URL{
			Path: "/status-399",
		}
		c := &gin.Context{
			Writer: &fakeResponseWriter{399}, // Edge case: just below 400
			Request: &http.Request{
				Header: make(http.Header),
				URL:    &test_url,
				Method: http.MethodPatch,
			},
		}
		Log(c)
	})

	t.Run("status code 499", func(t *testing.T) {
		test_url := url.URL{
			Path: "/status-499",
		}
		c := &gin.Context{
			Writer: &fakeResponseWriter{499}, // Edge case: at upper bound of 4xx
			Request: &http.Request{
				Header: make(http.Header),
				URL:    &test_url,
				Method: http.MethodDelete,
			},
		}
		Log(c)
	})

	t.Run("global server request parameters with alicloud vendor", func(t *testing.T) {
		test_url := url.URL{
			Path: "/api/test-alicloud",
		}
		c := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusOK},
			Request: &http.Request{
				Header: make(http.Header),
				URL:    &test_url,
				Method: http.MethodPost,
			},
		}

		reqParams := structs.RequestParameters{
			CommonReqInfo: structs.CommonReqInfo{
				Vendor: constants.AliCloud,
				Region: "cn-beijing",
			},
			Headers:     make(map[string]string),
			QueryParams: make(map[string]string),
			PathParams:  []gin.Param{},
			FormParams:  make(map[string]string),
			BodyParams:  "",
		}
		c.Set(structs.GlobalServerRequestParametersKey, reqParams)

		Log(c)
		assert.True(t, c.Keys[string(constants.OpenAPIHeaderRequestId)] != "")
	})

	t.Run("request with complex URL", func(t *testing.T) {
		test_url := url.URL{
			Scheme:   "https",
			Host:     "example.com",
			Path:     "/api/v1/complex/path",
			RawQuery: "param1=value1&param2=value2",
		}
		c := &gin.Context{
			Writer: &fakeResponseWriter{http.StatusCreated},
			Request: &http.Request{
				Header: make(http.Header),
				URL:    &test_url,
				Method: http.MethodPut,
			},
		}
		Log(c)
		assert.True(t, c.Keys[string(constants.OpenAPIHeaderRequestId)] != "")
	})
}

func Test_withErrorContext(t *testing.T) {
	t.Run("no errors or context", func(t *testing.T) {
		req := &http.Request{
			Header: make(http.Header),
		}
		c, _ := gin.CreateTestContext(&fakeResponseWriter{})
		c.Request = req.WithContext(context.Background())

		initialFields := []zap.Field{zap.String("test", "value")}
		result := withErrorContext(c, initialFields)

		assert.Equal(t, 1, len(result))
		assert.Equal(t, "test", result[0].Key)
	})

	t.Run("with gin errors", func(t *testing.T) {
		req := &http.Request{
			Header: make(http.Header),
		}
		c, _ := gin.CreateTestContext(&fakeResponseWriter{})
		c.Request = req.WithContext(context.Background())
		c.Error(assert.AnError)

		initialFields := []zap.Field{zap.String("test", "value")}
		result := withErrorContext(c, initialFields)

		assert.Equal(t, 2, len(result))
		assert.Equal(t, "errors", result[1].Key)
	})

	t.Run("with request result context", func(t *testing.T) {
		test_url := url.URL{Path: "/test"}
		requestResult := map[string]interface{}{
			"status": "failed",
			"reason": "validation error",
		}
		ctx := context.WithValue(context.Background(), constants.RequestResultCtxKey, requestResult)
		req := &http.Request{
			Header: make(http.Header),
			URL:    &test_url,
		}
		engine := gin.New()
		engine.ContextWithFallback = true
		c := gin.CreateTestContextOnly(&fakeResponseWriter{}, engine)
		c.Request = req.WithContext(ctx)

		initialFields := []zap.Field{zap.String("test", "value")}
		result := withErrorContext(c, initialFields)

		assert.Equal(t, 2, len(result))
		assert.Equal(t, "request_result", result[1].Key)
	})

	t.Run("with body bytes", func(t *testing.T) {
		req := &http.Request{
			Header: make(http.Header),
		}
		c, _ := gin.CreateTestContext(&fakeResponseWriter{})
		c.Request = req.WithContext(context.Background())
		bodyContent := []byte(`{"key":"value"}`)
		c.Set(gin.BodyBytesKey, bodyContent)

		initialFields := []zap.Field{zap.String("test", "value")}
		result := withErrorContext(c, initialFields)

		assert.Equal(t, 2, len(result))
		assert.Equal(t, "body", result[1].Key)
	})

	t.Run("with body bytes wrong type", func(t *testing.T) {
		req := &http.Request{
			Header: make(http.Header),
		}
		c, _ := gin.CreateTestContext(&fakeResponseWriter{})
		c.Request = req.WithContext(context.Background())
		c.Set(gin.BodyBytesKey, "not a byte slice")

		initialFields := []zap.Field{zap.String("test", "value")}
		result := withErrorContext(c, initialFields)

		assert.Equal(t, 1, len(result))
	})

	t.Run("with all error contexts", func(t *testing.T) {
		test_url := url.URL{Path: "/test"}
		requestResult := map[string]interface{}{
			"status": "error",
			"code":   500,
		}
		ctx := context.WithValue(context.Background(), constants.RequestResultCtxKey, requestResult)
		req := &http.Request{
			Header: make(http.Header),
			URL:    &test_url,
		}
		engine := gin.New()
		engine.ContextWithFallback = true
		c := gin.CreateTestContextOnly(&fakeResponseWriter{}, engine)
		c.Request = req.WithContext(ctx)

		c.Error(assert.AnError)

		bodyContent := []byte(`{"error":"internal server error"}`)
		c.Set(gin.BodyBytesKey, bodyContent)

		initialFields := []zap.Field{zap.String("test", "value")}
		result := withErrorContext(c, initialFields)

		assert.Equal(t, 4, len(result))
		assert.Equal(t, "test", result[0].Key)
		assert.Equal(t, "errors", result[1].Key)
		assert.Equal(t, "request_result", result[2].Key)
		assert.Equal(t, "body", result[3].Key)
	})

	t.Run("with empty body bytes", func(t *testing.T) {
		req := &http.Request{
			Header: make(http.Header),
		}
		c, _ := gin.CreateTestContext(&fakeResponseWriter{})
		c.Request = req.WithContext(context.Background())
		bodyContent := []byte{}
		c.Set(gin.BodyBytesKey, bodyContent)

		initialFields := []zap.Field{zap.String("test", "value")}
		result := withErrorContext(c, initialFields)

		assert.Equal(t, 2, len(result))
		assert.Equal(t, "body", result[1].Key)
	})

	t.Run("with nil request result", func(t *testing.T) {
		test_url := url.URL{Path: "/test"}
		ctx := context.WithValue(context.Background(), constants.RequestResultCtxKey, nil)
		req := &http.Request{
			Header: make(http.Header),
			URL:    &test_url,
		}
		c, _ := gin.CreateTestContext(&fakeResponseWriter{})
		c.Request = req.WithContext(ctx)

		initialFields := []zap.Field{zap.String("test", "value")}
		result := withErrorContext(c, initialFields)

		assert.Equal(t, 1, len(result))
	})

	t.Run("with multiple gin errors", func(t *testing.T) {
		req := &http.Request{
			Header: make(http.Header),
		}
		c, _ := gin.CreateTestContext(&fakeResponseWriter{})
		c.Request = req.WithContext(context.Background())
		c.Error(assert.AnError)
		c.Error(http.ErrServerClosed)

		initialFields := []zap.Field{}
		result := withErrorContext(c, initialFields)

		assert.Equal(t, 1, len(result))
		assert.Equal(t, "errors", result[0].Key)
	})
}
