package httpclient

import (
	"context"
	"errors"
	"fmt"
	"net"
	"net/url"
	"testing"
)

func TestHTTPError(t *testing.T) {
	t.Run("error with body", func(t *testing.T) {
		err := &HTTPError{
			StatusCode: 404,
			Status:     "Not Found",
			Body:       []byte(`{"error": "resource not found"}`),
			URL:        "https://example.com/api/resource",
		}

		errStr := err.Error()
		if errStr == "" {
			t.Error("expected non-empty error string")
		}
		if !contains(errStr, "404") {
			t.Error("expected error to contain status code")
		}
		if !contains(errStr, "Not Found") {
			t.Error("expected error to contain status text")
		}
		if !contains(errStr, "resource not found") {
			t.Error("expected error to contain body")
		}
		if !contains(errStr, "example.com") {
			t.Error("expected error to contain URL")
		}
	})

	t.Run("error without body", func(t *testing.T) {
		err := &HTTPError{
			StatusCode: 500,
			Status:     "Internal Server Error",
			Body:       nil,
			URL:        "https://example.com/api",
		}

		errStr := err.Error()
		if !contains(errStr, "500") {
			t.Error("expected error to contain status code")
		}
		if !contains(errStr, "Internal Server Error") {
			t.Error("expected error to contain status text")
		}
	})

	t.Run("error with long body truncates", func(t *testing.T) {
		longBody := make([]byte, 500)
		for i := range longBody {
			longBody[i] = 'a'
		}

		err := &HTTPError{
			StatusCode: 400,
			Status:     "Bad Request",
			Body:       longBody,
			URL:        "https://example.com/api",
		}

		errStr := err.Error()
		if !contains(errStr, "...") {
			t.Error("expected error to truncate long body")
		}
	})
}

func TestIsHTTPError(t *testing.T) {
	t.Run("is HTTPError", func(t *testing.T) {
		err := &HTTPError{StatusCode: 404}
		if !IsHTTPError(err) {
			t.Error("expected IsHTTPError to return true")
		}
	})

	t.Run("wrapped HTTPError", func(t *testing.T) {
		err := fmt.Errorf("wrapped: %w", &HTTPError{StatusCode: 404})
		if !IsHTTPError(err) {
			t.Error("expected IsHTTPError to return true for wrapped error")
		}
	})

	t.Run("not HTTPError", func(t *testing.T) {
		err := errors.New("some error")
		if IsHTTPError(err) {
			t.Error("expected IsHTTPError to return false")
		}
	})

	t.Run("nil error", func(t *testing.T) {
		if IsHTTPError(nil) {
			t.Error("expected IsHTTPError to return false for nil")
		}
	})
}

func TestGetHTTPError(t *testing.T) {
	t.Run("get HTTPError", func(t *testing.T) {
		originalErr := &HTTPError{StatusCode: 404, Status: "Not Found"}
		err := fmt.Errorf("wrapped: %w", originalErr)

		httpErr, ok := GetHTTPError(err)
		if !ok {
			t.Error("expected GetHTTPError to return true")
		}
		if httpErr.StatusCode != 404 {
			t.Errorf("expected status code 404, got %d", httpErr.StatusCode)
		}
	})

	t.Run("not HTTPError", func(t *testing.T) {
		err := errors.New("some error")
		_, ok := GetHTTPError(err)
		if ok {
			t.Error("expected GetHTTPError to return false")
		}
	})
}

func TestIsTimeout(t *testing.T) {
	t.Run("ErrTimeout", func(t *testing.T) {
		err := fmt.Errorf("wrapped: %w", ErrTimeout)
		if !IsTimeout(err) {
			t.Error("expected IsTimeout to return true for ErrTimeout")
		}
	})

	t.Run("context.DeadlineExceeded", func(t *testing.T) {
		if !IsTimeout(context.DeadlineExceeded) {
			t.Error("expected IsTimeout to return true for DeadlineExceeded")
		}
	})

	t.Run("net.Error with timeout", func(t *testing.T) {
		err := &mockNetError{timeout: true}
		if !IsTimeout(err) {
			t.Error("expected IsTimeout to return true for net.Error timeout")
		}
	})

	t.Run("url.Error with timeout", func(t *testing.T) {
		err := &url.Error{
			Op:  "Get",
			URL: "https://example.com",
			Err: &mockNetError{timeout: true},
		}
		if !IsTimeout(err) {
			t.Error("expected IsTimeout to return true for url.Error timeout")
		}
	})

	t.Run("regular error", func(t *testing.T) {
		err := errors.New("some error")
		if IsTimeout(err) {
			t.Error("expected IsTimeout to return false for regular error")
		}
	})

	t.Run("nil error", func(t *testing.T) {
		if IsTimeout(nil) {
			t.Error("expected IsTimeout to return false for nil")
		}
	})
}

func TestIsCanceled(t *testing.T) {
	t.Run("ErrCanceled", func(t *testing.T) {
		err := fmt.Errorf("wrapped: %w", ErrCanceled)
		if !IsCanceled(err) {
			t.Error("expected IsCanceled to return true for ErrCanceled")
		}
	})

	t.Run("context.Canceled", func(t *testing.T) {
		if !IsCanceled(context.Canceled) {
			t.Error("expected IsCanceled to return true for context.Canceled")
		}
	})

	t.Run("regular error", func(t *testing.T) {
		err := errors.New("some error")
		if IsCanceled(err) {
			t.Error("expected IsCanceled to return false for regular error")
		}
	})

	t.Run("nil error", func(t *testing.T) {
		if IsCanceled(nil) {
			t.Error("expected IsCanceled to return false for nil")
		}
	})
}

func TestIsConnectionError(t *testing.T) {
	t.Run("ErrConnectionRefused", func(t *testing.T) {
		err := fmt.Errorf("wrapped: %w", ErrConnectionRefused)
		if !IsConnectionError(err) {
			t.Error("expected IsConnectionError to return true for ErrConnectionRefused")
		}
	})

	t.Run("ErrConnectionReset", func(t *testing.T) {
		err := fmt.Errorf("wrapped: %w", ErrConnectionReset)
		if !IsConnectionError(err) {
			t.Error("expected IsConnectionError to return true for ErrConnectionReset")
		}
	})

	t.Run("ErrDNSLookupFailed", func(t *testing.T) {
		err := fmt.Errorf("wrapped: %w", ErrDNSLookupFailed)
		if !IsConnectionError(err) {
			t.Error("expected IsConnectionError to return true for ErrDNSLookupFailed")
		}
	})

	t.Run("regular error", func(t *testing.T) {
		err := errors.New("some error")
		if IsConnectionError(err) {
			t.Error("expected IsConnectionError to return false for regular error")
		}
	})

	t.Run("nil error", func(t *testing.T) {
		if IsConnectionError(nil) {
			t.Error("expected IsConnectionError to return false for nil")
		}
	})
}

func TestIsRetryable(t *testing.T) {
	t.Run("timeout error is retryable", func(t *testing.T) {
		err := fmt.Errorf("wrapped: %w", ErrTimeout)
		if !IsRetryable(err) {
			t.Error("expected timeout error to be retryable")
		}
	})

	t.Run("connection refused is retryable", func(t *testing.T) {
		err := fmt.Errorf("wrapped: %w", ErrConnectionRefused)
		if !IsRetryable(err) {
			t.Error("expected connection refused to be retryable")
		}
	})

	t.Run("5xx HTTP error is retryable", func(t *testing.T) {
		err := &HTTPError{StatusCode: 500}
		if !IsRetryable(err) {
			t.Error("expected 500 error to be retryable")
		}

		err = &HTTPError{StatusCode: 502}
		if !IsRetryable(err) {
			t.Error("expected 502 error to be retryable")
		}

		err = &HTTPError{StatusCode: 503}
		if !IsRetryable(err) {
			t.Error("expected 503 error to be retryable")
		}
	})

	t.Run("429 rate limit is retryable", func(t *testing.T) {
		err := &HTTPError{StatusCode: 429}
		if !IsRetryable(err) {
			t.Error("expected 429 error to be retryable")
		}
	})

	t.Run("4xx HTTP error is not retryable", func(t *testing.T) {
		err := &HTTPError{StatusCode: 400}
		if IsRetryable(err) {
			t.Error("expected 400 error to not be retryable")
		}

		err = &HTTPError{StatusCode: 404}
		if IsRetryable(err) {
			t.Error("expected 404 error to not be retryable")
		}
	})

	t.Run("regular error is not retryable", func(t *testing.T) {
		err := errors.New("some error")
		if IsRetryable(err) {
			t.Error("expected regular error to not be retryable")
		}
	})

	t.Run("nil error is not retryable", func(t *testing.T) {
		if IsRetryable(nil) {
			t.Error("expected nil to not be retryable")
		}
	})
}

func TestWrapError(t *testing.T) {
	t.Run("nil error", func(t *testing.T) {
		err := wrapError(nil)
		if err != nil {
			t.Error("expected nil for nil input")
		}
	})

	t.Run("context.Canceled", func(t *testing.T) {
		err := wrapError(context.Canceled)
		if !IsCanceled(err) {
			t.Error("expected wrapped error to be canceled")
		}
	})

	t.Run("context.DeadlineExceeded", func(t *testing.T) {
		err := wrapError(context.DeadlineExceeded)
		if !IsTimeout(err) {
			t.Error("expected wrapped error to be timeout")
		}
	})

	t.Run("url.Error with timeout", func(t *testing.T) {
		urlErr := &url.Error{
			Op:  "Get",
			URL: "https://example.com",
			Err: &mockNetError{timeout: true},
		}
		err := wrapError(urlErr)
		if !IsTimeout(err) {
			t.Error("expected wrapped error to be timeout")
		}
	})

	t.Run("DNS error", func(t *testing.T) {
		dnsErr := &net.DNSError{
			Err:  "no such host",
			Name: "example.com",
		}
		err := wrapError(dnsErr)
		if !errors.Is(err, ErrDNSLookupFailed) {
			t.Error("expected wrapped error to be DNS lookup failed")
		}
	})

	t.Run("regular error passes through", func(t *testing.T) {
		originalErr := errors.New("some error")
		err := wrapError(originalErr)
		if err != originalErr {
			t.Error("expected regular error to pass through unchanged")
		}
	})
}

// mockNetError implements net.Error for testing
type mockNetError struct {
	timeout   bool
	temporary bool
}

func (e *mockNetError) Error() string   { return "mock net error" }
func (e *mockNetError) Timeout() bool   { return e.timeout }
func (e *mockNetError) Temporary() bool { return e.temporary }

// contains checks if s contains substr
func contains(s, substr string) bool {
	return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr))
}

func containsHelper(s, substr string) bool {
	for i := 0; i <= len(s)-len(substr); i++ {
		if s[i:i+len(substr)] == substr {
			return true
		}
	}
	return false
}
