package middleware

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/labstack/echo/v4"
	"github.com/stretchr/testify/assert"
)

func TestRequestID_ValidID_Preserved(t *testing.T) {
	e := echo.New()
	mw := RequestIDMiddleware()
	handler := mw(func(c echo.Context) error {
		return c.String(http.StatusOK, GetRequestID(c))
	})

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.Header.Set(HeaderXRequestID, "abc-123-def")
	rec := httptest.NewRecorder()
	c := e.NewContext(req, rec)

	err := handler(c)
	assert.NoError(t, err)
	assert.Equal(t, "abc-123-def", rec.Body.String())
	assert.Equal(t, "abc-123-def", rec.Header().Get(HeaderXRequestID))
}

func TestRequestID_Empty_GeneratesNew(t *testing.T) {
	e := echo.New()
	mw := RequestIDMiddleware()
	handler := mw(func(c echo.Context) error {
		return c.String(http.StatusOK, GetRequestID(c))
	})

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	// No X-Request-ID header
	rec := httptest.NewRecorder()
	c := e.NewContext(req, rec)

	err := handler(c)
	assert.NoError(t, err)
	// Should be a UUID (36 chars: 8-4-4-4-12)
	assert.Len(t, rec.Body.String(), 36)
}

func TestRequestID_ControlChars_Sanitized(t *testing.T) {
	// SEC-29: Client-supplied X-Request-ID with control characters must be rejected.
	tests := []struct {
		name    string
		inputID string
	}{
		{"newline injection", "abc\ndef"},
		{"carriage return", "abc\rdef"},
		{"null byte", "abc\x00def"},
		{"tab character", "abc\tdef"},
		{"html tags", "abc<script>alert(1)</script>"},
		{"spaces", "abc def"},
		{"semicolons", "abc;def"},
		{"unicode", "abc\u200bdef"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			e := echo.New()
			mw := RequestIDMiddleware()
			handler := mw(func(c echo.Context) error {
				return c.String(http.StatusOK, GetRequestID(c))
			})

			req := httptest.NewRequest(http.MethodGet, "/", nil)
			req.Header.Set(HeaderXRequestID, tt.inputID)
			rec := httptest.NewRecorder()
			c := e.NewContext(req, rec)

			err := handler(c)
			assert.NoError(t, err)
			// The malicious ID should be replaced with a generated UUID
			assert.NotEqual(t, tt.inputID, rec.Body.String(),
				"control chars should be rejected, got original ID back")
			assert.Len(t, rec.Body.String(), 36, "should be a generated UUID")
		})
	}
}

func TestRequestID_TooLong_Sanitized(t *testing.T) {
	// SEC-29: X-Request-ID longer than 64 chars should be rejected.
	e := echo.New()
	mw := RequestIDMiddleware()
	handler := mw(func(c echo.Context) error {
		return c.String(http.StatusOK, GetRequestID(c))
	})

	longID := strings.Repeat("a", 65)
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.Header.Set(HeaderXRequestID, longID)
	rec := httptest.NewRecorder()
	c := e.NewContext(req, rec)

	err := handler(c)
	assert.NoError(t, err)
	assert.NotEqual(t, longID, rec.Body.String(), "overly long ID should be replaced")
	assert.Len(t, rec.Body.String(), 36, "should be a generated UUID")
}

func TestRequestID_MaxLength_Accepted(t *testing.T) {
	// Exactly 64 chars of valid characters should be accepted
	e := echo.New()
	mw := RequestIDMiddleware()
	handler := mw(func(c echo.Context) error {
		return c.String(http.StatusOK, GetRequestID(c))
	})

	maxID := strings.Repeat("a", 64)
	req := httptest.NewRequest(http.MethodGet, "/", nil)
	req.Header.Set(HeaderXRequestID, maxID)
	rec := httptest.NewRecorder()
	c := e.NewContext(req, rec)

	err := handler(c)
	assert.NoError(t, err)
	assert.Equal(t, maxID, rec.Body.String(), "64-char valid ID should be accepted")
}
