package httperror

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestNewValidationError(t *testing.T) {
	fieldErrors := map[string]string{
		"email": "Email is required",
		"password": "Password must be at least 8 characters",
	}

	problem := NewValidationError(fieldErrors)

	if problem.Type != TypeValidationError {
		t.Errorf("Expected type %s, got %s", TypeValidationError, problem.Type)
	}

	if problem.Status != http.StatusBadRequest {
		t.Errorf("Expected status %d, got %d", http.StatusBadRequest, problem.Status)
	}

	if problem.Title != "Validation Failed" {
		t.Errorf("Expected title 'Validation Failed', got '%s'", problem.Title)
	}

	if len(problem.Errors) != 2 {
		t.Errorf("Expected 2 field errors, got %d", len(problem.Errors))
	}

	if problem.Errors["email"] != "Email is required" {
		t.Errorf("Expected email error, got '%s'", problem.Errors["email"])
	}

	if problem.Timestamp == "" {
		t.Error("Expected timestamp to be set")
	}
}

func TestNewValidationError_Empty(t *testing.T) {
	problem := NewValidationError(map[string]string{})

	if problem.Detail != "Validation failed." {
		t.Errorf("Expected detail 'Validation failed.', got '%s'", problem.Detail)
	}
}

func TestNewBadRequestError(t *testing.T) {
	detail := "Invalid request payload"
	problem := NewBadRequestError(detail)

	if problem.Type != TypeBadRequest {
		t.Errorf("Expected type %s, got %s", TypeBadRequest, problem.Type)
	}

	if problem.Status != http.StatusBadRequest {
		t.Errorf("Expected status %d, got %d", http.StatusBadRequest, problem.Status)
	}

	if problem.Detail != detail {
		t.Errorf("Expected detail '%s', got '%s'", detail, problem.Detail)
	}
}

func TestNewUnauthorizedError(t *testing.T) {
	detail := "Invalid token"
	problem := NewUnauthorizedError(detail)

	if problem.Type != TypeUnauthorized {
		t.Errorf("Expected type %s, got %s", TypeUnauthorized, problem.Type)
	}

	if problem.Status != http.StatusUnauthorized {
		t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, problem.Status)
	}

	if problem.Detail != detail {
		t.Errorf("Expected detail '%s', got '%s'", detail, problem.Detail)
	}
}

func TestNewUnauthorizedError_DefaultMessage(t *testing.T) {
	problem := NewUnauthorizedError("")

	if problem.Detail != "Authentication is required to access this resource." {
		t.Errorf("Expected default detail message, got '%s'", problem.Detail)
	}
}

func TestNewForbiddenError(t *testing.T) {
	detail := "Insufficient permissions"
	problem := NewForbiddenError(detail)

	if problem.Type != TypeForbidden {
		t.Errorf("Expected type %s, got %s", TypeForbidden, problem.Type)
	}

	if problem.Status != http.StatusForbidden {
		t.Errorf("Expected status %d, got %d", http.StatusForbidden, problem.Status)
	}
}

func TestNewNotFoundError(t *testing.T) {
	problem := NewNotFoundError("User")

	if problem.Type != TypeNotFound {
		t.Errorf("Expected type %s, got %s", TypeNotFound, problem.Type)
	}

	if problem.Status != http.StatusNotFound {
		t.Errorf("Expected status %d, got %d", http.StatusNotFound, problem.Status)
	}

	if problem.Detail != "User not found." {
		t.Errorf("Expected detail 'User not found.', got '%s'", problem.Detail)
	}
}

func TestNewConflictError(t *testing.T) {
	detail := "Email already exists"
	problem := NewConflictError(detail)

	if problem.Type != TypeConflict {
		t.Errorf("Expected type %s, got %s", TypeConflict, problem.Type)
	}

	if problem.Status != http.StatusConflict {
		t.Errorf("Expected status %d, got %d", http.StatusConflict, problem.Status)
	}
}

func TestNewInternalServerError(t *testing.T) {
	detail := "Database connection failed"
	problem := NewInternalServerError(detail)

	if problem.Type != TypeInternalError {
		t.Errorf("Expected type %s, got %s", TypeInternalError, problem.Type)
	}

	if problem.Status != http.StatusInternalServerError {
		t.Errorf("Expected status %d, got %d", http.StatusInternalServerError, problem.Status)
	}
}

func TestNewServiceUnavailableError(t *testing.T) {
	problem := NewServiceUnavailableError("")

	if problem.Type != TypeServiceUnavailable {
		t.Errorf("Expected type %s, got %s", TypeServiceUnavailable, problem.Type)
	}

	if problem.Status != http.StatusServiceUnavailable {
		t.Errorf("Expected status %d, got %d", http.StatusServiceUnavailable, problem.Status)
	}
}

func TestWithInstance(t *testing.T) {
	problem := NewBadRequestError("Test")
	instance := "/api/v1/test"

	problem.WithInstance(instance)

	if problem.Instance != instance {
		t.Errorf("Expected instance '%s', got '%s'", instance, problem.Instance)
	}
}

func TestWithTraceID(t *testing.T) {
	problem := NewBadRequestError("Test")
	traceID := "trace-123"

	problem.WithTraceID(traceID)

	if problem.TraceID != traceID {
		t.Errorf("Expected traceID '%s', got '%s'", traceID, problem.TraceID)
	}
}

func TestWithError(t *testing.T) {
	problem := NewBadRequestError("Test")

	problem.WithError("email", "Email is required")
	problem.WithError("password", "Password is required")

	if len(problem.Errors) != 2 {
		t.Errorf("Expected 2 errors, got %d", len(problem.Errors))
	}

	if problem.Errors["email"] != "Email is required" {
		t.Errorf("Expected email error, got '%s'", problem.Errors["email"])
	}
}

func TestProblemDetailError(t *testing.T) {
	detail := "Test detail"
	problem := NewBadRequestError(detail)

	if problem.Error() != detail {
		t.Errorf("Expected Error() to return detail, got '%s'", problem.Error())
	}

	// Test with no detail
	problem2 := &ProblemDetail{
		Title: "Test Title",
	}

	if problem2.Error() != "Test Title" {
		t.Errorf("Expected Error() to return title, got '%s'", problem2.Error())
	}
}

func TestExtractRequestID_FromContext(t *testing.T) {
	req := httptest.NewRequest("GET", "/test", nil)

	// Note: In real code, request ID would be set by middleware
	// For testing, we'll test the empty case
	requestID := ExtractRequestID(req)

	// Should return empty string when no request ID is present
	if requestID != "" {
		t.Errorf("Expected empty string, got '%s'", requestID)
	}
}

func TestExtractRequestID_FromHeader(t *testing.T) {
	req := httptest.NewRequest("GET", "/test", nil)
	req.Header.Set("X-Request-ID", "test-request-123")

	requestID := ExtractRequestID(req)

	if requestID != "test-request-123" {
		t.Errorf("Expected 'test-request-123', got '%s'", requestID)
	}
}

func TestRespondWithProblem(t *testing.T) {
	problem := NewValidationError(map[string]string{
		"email": "Email is required",
	})
	problem.WithInstance("/api/v1/test")
	problem.WithTraceID("trace-123")

	w := httptest.NewRecorder()
	RespondWithProblem(w, problem)

	// Check status code
	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code)
	}

	// Check content type
	contentType := w.Header().Get("Content-Type")
	if contentType != "application/problem+json" {
		t.Errorf("Expected Content-Type 'application/problem+json', got '%s'", contentType)
	}

	// Check JSON response
	var response ProblemDetail
	if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
		t.Fatalf("Failed to decode response: %v", err)
	}

	if response.Type != TypeValidationError {
		t.Errorf("Expected type %s, got %s", TypeValidationError, response.Type)
	}

	if response.Instance != "/api/v1/test" {
		t.Errorf("Expected instance '/api/v1/test', got '%s'", response.Instance)
	}

	if response.TraceID != "trace-123" {
		t.Errorf("Expected traceID 'trace-123', got '%s'", response.TraceID)
	}

	if len(response.Errors) != 1 {
		t.Errorf("Expected 1 error, got %d", len(response.Errors))
	}
}

func TestRespondWithError_ProblemDetail(t *testing.T) {
	req := httptest.NewRequest("GET", "/api/v1/test", nil)
	w := httptest.NewRecorder()

	problem := NewBadRequestError("Test error")
	RespondWithError(w, req, problem)

	// Check status code
	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code)
	}

	// Check that instance was set
	var response ProblemDetail
	json.NewDecoder(w.Body).Decode(&response)

	if response.Instance != "/api/v1/test" {
		t.Errorf("Expected instance to be set automatically, got '%s'", response.Instance)
	}
}

func TestRespondWithError_StandardError(t *testing.T) {
	req := httptest.NewRequest("GET", "/api/v1/test", nil)
	w := httptest.NewRecorder()

	err := &customError{message: "Custom error"}
	RespondWithError(w, req, err)

	// Check status code (should be 500 for standard errors)
	if w.Code != http.StatusInternalServerError {
		t.Errorf("Expected status %d, got %d", http.StatusInternalServerError, w.Code)
	}

	// Check that it was wrapped in a ProblemDetail
	var response ProblemDetail
	json.NewDecoder(w.Body).Decode(&response)

	if response.Type != TypeInternalError {
		t.Errorf("Expected type %s, got %s", TypeInternalError, response.Type)
	}

	if response.Detail != "Custom error" {
		t.Errorf("Expected detail 'Custom error', got '%s'", response.Detail)
	}
}

// Helper type for testing standard error handling
type customError struct {
	message string
}

func (e *customError) Error() string {
	return e.message
}

func TestChaining(t *testing.T) {
	// Test method chaining
	problem := NewBadRequestError("Test").
		WithInstance("/api/v1/test").
		WithTraceID("trace-123").
		WithError("field1", "error1").
		WithError("field2", "error2")

	if problem.Instance != "/api/v1/test" {
		t.Error("Instance not set correctly through chaining")
	}

	if problem.TraceID != "trace-123" {
		t.Error("TraceID not set correctly through chaining")
	}

	if len(problem.Errors) != 2 {
		t.Error("Errors not set correctly through chaining")
	}
}
