// Package httpclient provides property-based tests for HTTP client timeout handling.
package httpclient

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

	"pgregory.net/rapid"
)

// TestHTTPTimeoutErrorProperty tests Property 15: HTTP 超时错误
// **Validates: Requirements 8.6**
//
// Property 15: HTTP 超时错误
// *对于任意* 超时的 HTTP 请求，应返回明确的超时错误类型。
func TestHTTPTimeoutErrorProperty(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		// Generate different timeout error scenarios
		timeoutType := rapid.IntRange(0, 4).Draw(t, "timeoutType")

		var err error
		switch timeoutType {
		case 0:
			// context.DeadlineExceeded
			err = context.DeadlineExceeded
		case 1:
			// net.Error with Timeout() = true
			err = &timeoutNetError{msg: "connection timed out"}
		case 2:
			// url.Error wrapping a timeout error
			err = &url.Error{
				Op:  "Get",
				URL: "https://example.com/api",
				Err: &timeoutNetError{msg: "dial timeout"},
			}
		case 3:
			// Wrapped ErrTimeout
			err = fmt.Errorf("operation failed: %w", ErrTimeout)
		case 4:
			// net.OpError with timeout
			err = &net.OpError{
				Op:  "dial",
				Net: "tcp",
				Err: &timeoutNetError{msg: "i/o timeout"},
			}
		}

		// Verify: IsTimeout should return true for all timeout errors
		if !IsTimeout(err) {
			t.Fatalf("IsTimeout should return true for timeout error type %d: %v", timeoutType, err)
		}
	})
}

// TestWrappedTimeoutErrorsAreIdentifiable tests that wrapped timeout errors
// are properly identified via IsTimeout()
// **Validates: Requirements 8.6**
func TestWrappedTimeoutErrorsAreIdentifiable(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		// Generate random wrapping depth
		wrapDepth := rapid.IntRange(1, 5).Draw(t, "wrapDepth")

		// Start with a base timeout error
		baseErrorType := rapid.IntRange(0, 2).Draw(t, "baseErrorType")
		var baseErr error
		switch baseErrorType {
		case 0:
			baseErr = ErrTimeout
		case 1:
			baseErr = context.DeadlineExceeded
		case 2:
			baseErr = &url.Error{
				Op:  "Get",
				URL: "https://example.com",
				Err: &timeoutNetError{msg: "timeout"},
			}
		}

		// Wrap the error multiple times
		wrappedErr := baseErr
		for i := 0; i < wrapDepth; i++ {
			wrappedErr = fmt.Errorf("layer %d: %w", i, wrappedErr)
		}

		// Verify: IsTimeout should still identify the wrapped timeout error
		if !IsTimeout(wrappedErr) {
			t.Fatalf("IsTimeout should return true for wrapped timeout error (depth=%d, baseType=%d): %v",
				wrapDepth, baseErrorType, wrappedErr)
		}
	})
}

// TestWrapErrorConvertsTimeoutErrors tests that wrapError properly converts
// low-level timeout errors to ErrTimeout
// **Validates: Requirements 8.6**
func TestWrapErrorConvertsTimeoutErrors(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		// Generate different low-level timeout error types
		errorType := rapid.IntRange(0, 3).Draw(t, "errorType")

		var inputErr error
		switch errorType {
		case 0:
			// context.DeadlineExceeded
			inputErr = context.DeadlineExceeded
		case 1:
			// url.Error with Timeout() = true
			inputErr = &url.Error{
				Op:  "Get",
				URL: "https://example.com/api",
				Err: &timeoutNetError{msg: "dial timeout"},
			}
		case 2:
			// net.Error with Timeout() = true
			inputErr = &timeoutNetError{msg: "connection timed out"}
		case 3:
			// net.OpError with timeout
			inputErr = &net.OpError{
				Op:  "read",
				Net: "tcp",
				Err: &timeoutNetError{msg: "i/o timeout"},
			}
		}

		// Wrap the error
		wrappedErr := wrapError(inputErr)

		// Verify: The wrapped error should be identifiable as a timeout
		if !IsTimeout(wrappedErr) {
			t.Fatalf("wrapError should produce timeout-identifiable error for type %d: input=%v, wrapped=%v",
				errorType, inputErr, wrappedErr)
		}

		// Verify: For context.DeadlineExceeded and url.Error with timeout,
		// the wrapped error should contain ErrTimeout
		if errorType == 0 || errorType == 1 {
			if !errors.Is(wrappedErr, ErrTimeout) {
				t.Fatalf("wrapError should wrap with ErrTimeout for type %d: %v", errorType, wrappedErr)
			}
		}
	})
}

// TestNonTimeoutErrorsAreNotIdentifiedAsTimeout tests that non-timeout errors
// are not incorrectly identified as timeouts
// **Validates: Requirements 8.6**
func TestNonTimeoutErrorsAreNotIdentifiedAsTimeout(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		// Generate different non-timeout error types
		errorType := rapid.IntRange(0, 5).Draw(t, "errorType")
		errorMsg := rapid.StringMatching(`[a-z]{5,20}`).Draw(t, "errorMsg")

		var err error
		switch errorType {
		case 0:
			// Simple error
			err = errors.New(errorMsg)
		case 1:
			// context.Canceled (not a timeout)
			err = context.Canceled
		case 2:
			// ErrConnectionRefused
			err = fmt.Errorf("connection error: %w", ErrConnectionRefused)
		case 3:
			// ErrDNSLookupFailed
			err = fmt.Errorf("dns error: %w", ErrDNSLookupFailed)
		case 4:
			// ErrConnectionReset
			err = fmt.Errorf("reset error: %w", ErrConnectionReset)
		case 5:
			// net.Error with Timeout() = false
			err = &nonTimeoutNetError{msg: errorMsg}
		}

		// Verify: IsTimeout should return false for non-timeout errors
		if IsTimeout(err) {
			t.Fatalf("IsTimeout should return false for non-timeout error type %d: %v", errorType, err)
		}
	})
}

// TestContextDeadlineExceededIsTimeout tests that context.DeadlineExceeded
// is always recognized as a timeout error
// **Validates: Requirements 8.6**
func TestContextDeadlineExceededIsTimeout(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		// Generate random context with timeout
		timeoutMs := rapid.IntRange(1, 100).Draw(t, "timeoutMs")
		timeout := time.Duration(timeoutMs) * time.Millisecond

		ctx, cancel := context.WithTimeout(context.Background(), timeout)
		defer cancel()

		// Wait for context to expire
		<-ctx.Done()

		// Get the context error
		err := ctx.Err()

		// Verify: The context error should be identified as a timeout
		if !IsTimeout(err) {
			t.Fatalf("IsTimeout should return true for expired context error: %v", err)
		}

		// Verify: wrapError should wrap it with ErrTimeout
		wrappedErr := wrapError(err)
		if !errors.Is(wrappedErr, ErrTimeout) {
			t.Fatalf("wrapError should wrap context.DeadlineExceeded with ErrTimeout: %v", wrappedErr)
		}
	})
}

// TestURLErrorTimeoutDetection tests that url.Error with timeout is properly detected
// **Validates: Requirements 8.6**
func TestURLErrorTimeoutDetection(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		// Generate random URL components
		host := rapid.StringMatching(`[a-z]{3,10}\.(com|org|net)`).Draw(t, "host")
		path := rapid.StringMatching(`/[a-z]{1,5}(/[a-z]{1,5})?`).Draw(t, "path")
		op := rapid.SampledFrom([]string{"Get", "Post", "Put", "Delete", "Head"}).Draw(t, "op")

		urlStr := fmt.Sprintf("https://%s%s", host, path)

		// Create url.Error with timeout
		urlErr := &url.Error{
			Op:  op,
			URL: urlStr,
			Err: &timeoutNetError{msg: "dial tcp: i/o timeout"},
		}

		// Verify: IsTimeout should return true
		if !IsTimeout(urlErr) {
			t.Fatalf("IsTimeout should return true for url.Error with timeout: %v", urlErr)
		}

		// Verify: wrapError should wrap it with ErrTimeout
		wrappedErr := wrapError(urlErr)
		if !errors.Is(wrappedErr, ErrTimeout) {
			t.Fatalf("wrapError should wrap url.Error timeout with ErrTimeout: %v", wrappedErr)
		}
	})
}

// TestNilErrorIsNotTimeout tests that nil error is not identified as timeout
// **Validates: Requirements 8.6**
func TestNilErrorIsNotTimeout(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		// Verify: IsTimeout should return false for nil
		if IsTimeout(nil) {
			t.Fatal("IsTimeout should return false for nil error")
		}

		// Verify: wrapError should return nil for nil input
		if wrapError(nil) != nil {
			t.Fatal("wrapError should return nil for nil input")
		}
	})
}

// timeoutNetError implements net.Error with Timeout() returning true
type timeoutNetError struct {
	msg string
}

func (e *timeoutNetError) Error() string   { return e.msg }
func (e *timeoutNetError) Timeout() bool   { return true }
func (e *timeoutNetError) Temporary() bool { return true }

// nonTimeoutNetError implements net.Error with Timeout() returning false
type nonTimeoutNetError struct {
	msg string
}

func (e *nonTimeoutNetError) Error() string   { return e.msg }
func (e *nonTimeoutNetError) Timeout() bool   { return false }
func (e *nonTimeoutNetError) Temporary() bool { return false }
