package tempproxy

import (
	"errors"
	"net"
	"testing"
	"time"
)

func TestErrorType_Constants(t *testing.T) {
	// Verify enum values are distinct
	values := map[ErrorType]bool{}
	errorTypes := []ErrorType{
		ErrorTypeUnknown,
		ErrorTypeTransport,
		ErrorTypeAuth,
		ErrorTypeSSH,
		ErrorTypeSensor,
		ErrorTypeTimeout,
		ErrorTypeNode,
	}

	for _, et := range errorTypes {
		if values[et] {
			t.Errorf("Duplicate ErrorType value: %d", et)
		}
		values[et] = true
	}

	// ErrorTypeUnknown should be 0 (default)
	if ErrorTypeUnknown != 0 {
		t.Errorf("ErrorTypeUnknown = %d, want 0", ErrorTypeUnknown)
	}
}

func TestProxyError_Error(t *testing.T) {
	tests := []struct {
		name     string
		err      ProxyError
		contains string
	}{
		{
			name: "with wrapped error",
			err: ProxyError{
				Message: "connection failed",
				Wrapped: errors.New("dial tcp: connection refused"),
			},
			contains: "connection failed: dial tcp: connection refused",
		},
		{
			name: "without wrapped error",
			err: ProxyError{
				Message: "operation timed out",
			},
			contains: "operation timed out",
		},
		{
			name: "with type and retryable",
			err: ProxyError{
				Type:      ErrorTypeSSH,
				Message:   "SSH connectivity issue",
				Retryable: true,
				Wrapped:   errors.New("ssh: handshake failed"),
			},
			contains: "SSH connectivity issue: ssh: handshake failed",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			result := tc.err.Error()
			if result != tc.contains {
				t.Errorf("Error() = %q, want %q", result, tc.contains)
			}
		})
	}
}

func TestProxyError_Unwrap(t *testing.T) {
	inner := errors.New("inner error")
	err := &ProxyError{
		Message: "outer error",
		Wrapped: inner,
	}

	unwrapped := err.Unwrap()
	if unwrapped != inner {
		t.Errorf("Unwrap() = %v, want %v", unwrapped, inner)
	}
}

func TestProxyError_Unwrap_Nil(t *testing.T) {
	err := &ProxyError{
		Message: "error without wrapped",
	}

	unwrapped := err.Unwrap()
	if unwrapped != nil {
		t.Errorf("Unwrap() = %v, want nil", unwrapped)
	}
}

func TestCalculateBackoff(t *testing.T) {
	tests := []struct {
		name        string
		attempt     int
		minExpected time.Duration
		maxExpected time.Duration
	}{
		{
			name:        "attempt 0",
			attempt:     0,
			minExpected: 90 * time.Millisecond,  // initialBackoff - 10% jitter
			maxExpected: 110 * time.Millisecond, // initialBackoff + 10% jitter
		},
		{
			name:        "attempt 1",
			attempt:     1,
			minExpected: 180 * time.Millisecond, // 200ms * 0.9
			maxExpected: 220 * time.Millisecond, // 200ms * 1.1
		},
		{
			name:        "attempt 2",
			attempt:     2,
			minExpected: 360 * time.Millisecond, // 400ms * 0.9
			maxExpected: 440 * time.Millisecond, // 400ms * 1.1
		},
		{
			name:        "negative attempt (treated as 0)",
			attempt:     -1,
			minExpected: 90 * time.Millisecond,
			maxExpected: 110 * time.Millisecond,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			// Run multiple times to account for jitter
			for i := 0; i < 10; i++ {
				result := calculateBackoff(tc.attempt)
				if result < tc.minExpected || result > tc.maxExpected {
					t.Errorf("calculateBackoff(%d) = %v, want between %v and %v",
						tc.attempt, result, tc.minExpected, tc.maxExpected)
				}
			}
		})
	}
}

func TestCalculateBackoff_CappedAtMax(t *testing.T) {
	// High attempt number should be capped at maxBackoff (10s) + jitter
	result := calculateBackoff(100)

	maxWithJitter := maxBackoff + time.Duration(float64(maxBackoff)*jitterFraction)
	if result > maxWithJitter {
		t.Errorf("calculateBackoff(100) = %v, should be capped at ~%v", result, maxBackoff)
	}
}

func TestContains(t *testing.T) {
	tests := []struct {
		name     string
		s        string
		substrs  []string
		expected bool
	}{
		{
			name:     "single match",
			s:        "connection refused",
			substrs:  []string{"refused"},
			expected: true,
		},
		{
			name:     "multiple substrs first match",
			s:        "ssh connection failed",
			substrs:  []string{"ssh", "timeout"},
			expected: true,
		},
		{
			name:     "multiple substrs second match",
			s:        "operation timeout",
			substrs:  []string{"ssh", "timeout"},
			expected: true,
		},
		{
			name:     "no match",
			s:        "success",
			substrs:  []string{"error", "failed"},
			expected: false,
		},
		{
			name:     "case insensitive upper",
			s:        "CONNECTION REFUSED",
			substrs:  []string{"connection"},
			expected: true,
		},
		{
			name:     "case insensitive mixed",
			s:        "SSH Error",
			substrs:  []string{"ssh error"},
			expected: true,
		},
		{
			name:     "empty string",
			s:        "",
			substrs:  []string{"test"},
			expected: false,
		},
		{
			name:     "empty substrs",
			s:        "test",
			substrs:  []string{},
			expected: false,
		},
		{
			name:     "exact match",
			s:        "error",
			substrs:  []string{"error"},
			expected: true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			result := contains(tc.s, tc.substrs...)
			if result != tc.expected {
				t.Errorf("contains(%q, %v) = %v, want %v", tc.s, tc.substrs, result, tc.expected)
			}
		})
	}
}

func TestClassifyError_NodeErrors(t *testing.T) {
	tests := []struct {
		name       string
		respError  string
		expectType ErrorType
		retryable  bool
	}{
		{
			name:       "rejected by validator",
			respError:  "rejected by validator: invalid node",
			expectType: ErrorTypeNode,
			retryable:  false,
		},
		{
			name:       "not in allowlist",
			respError:  "node not in allowlist",
			expectType: ErrorTypeNode,
			retryable:  false,
		},
		{
			name:       "node quote pattern",
			respError:  "node \"server1\" not found",
			expectType: ErrorTypeNode,
			retryable:  false,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			result := classifyError(nil, tc.respError)
			if result == nil {
				t.Fatal("classifyError returned nil")
			}
			if result.Type != tc.expectType {
				t.Errorf("Type = %v, want %v", result.Type, tc.expectType)
			}
			if result.Retryable != tc.retryable {
				t.Errorf("Retryable = %v, want %v", result.Retryable, tc.retryable)
			}
		})
	}
}

func TestClassifyError_AuthErrors(t *testing.T) {
	tests := []struct {
		respError string
	}{
		{"unauthorized"},
		{"method requires host-level privileges"},
		{"method requires admin capability"},
		{"missing admin capability for operation"},
	}

	for _, tc := range tests {
		t.Run(tc.respError, func(t *testing.T) {
			result := classifyError(nil, tc.respError)
			if result == nil {
				t.Fatal("classifyError returned nil")
			}
			if result.Type != ErrorTypeAuth {
				t.Errorf("Type = %v, want ErrorTypeAuth", result.Type)
			}
			if result.Retryable {
				t.Error("Auth errors should not be retryable")
			}
		})
	}
}

func TestClassifyError_SSHErrors(t *testing.T) {
	tests := []struct {
		respError string
	}{
		{"ssh: handshake failed"},
		{"connection reset by peer"},
		{"operation timeout exceeded"},
	}

	for _, tc := range tests {
		t.Run(tc.respError, func(t *testing.T) {
			result := classifyError(nil, tc.respError)
			if result == nil {
				t.Fatal("classifyError returned nil")
			}
			if result.Type != ErrorTypeSSH {
				t.Errorf("Type = %v, want ErrorTypeSSH", result.Type)
			}
			if !result.Retryable {
				t.Error("SSH errors should be retryable")
			}
		})
	}
}

func TestClassifyError_SensorErrors(t *testing.T) {
	tests := []struct {
		respError string
	}{
		{"sensor command failed"},
		{"temperature sensor unavailable"},
	}

	for _, tc := range tests {
		t.Run(tc.respError, func(t *testing.T) {
			result := classifyError(nil, tc.respError)
			if result == nil {
				t.Fatal("classifyError returned nil")
			}
			if result.Type != ErrorTypeSensor {
				t.Errorf("Type = %v, want ErrorTypeSensor", result.Type)
			}
			if result.Retryable {
				t.Error("Sensor errors should not be retryable")
			}
		})
	}
}

func TestClassifyError_RateLimitErrors(t *testing.T) {
	result := classifyError(nil, "rate limit exceeded")
	if result == nil {
		t.Fatal("classifyError returned nil")
	}
	if result.Type != ErrorTypeTransport {
		t.Errorf("Type = %v, want ErrorTypeTransport", result.Type)
	}
	if result.Retryable {
		t.Error("Rate limit errors should not be retryable")
	}
}

func TestClassifyError_TimeoutNetError(t *testing.T) {
	// Create a mock timeout error
	timeoutErr := &mockNetError{timeout: true}

	result := classifyError(timeoutErr, "")
	if result == nil {
		t.Fatal("classifyError returned nil")
	}
	if result.Type != ErrorTypeTimeout {
		t.Errorf("Type = %v, want ErrorTypeTimeout", result.Type)
	}
	if !result.Retryable {
		t.Error("Timeout errors should be retryable")
	}
}

func TestClassifyError_OpError(t *testing.T) {
	opErr := &net.OpError{
		Op:  "dial",
		Net: "unix",
		Err: errors.New("connection refused"),
	}

	result := classifyError(opErr, "")
	if result == nil {
		t.Fatal("classifyError returned nil")
	}
	if result.Type != ErrorTypeTransport {
		t.Errorf("Type = %v, want ErrorTypeTransport", result.Type)
	}
	if !result.Retryable {
		t.Error("Transport errors should be retryable")
	}
}

func TestClassifyError_NilInputs(t *testing.T) {
	result := classifyError(nil, "")
	if result != nil {
		t.Errorf("classifyError(nil, \"\") = %v, want nil", result)
	}
}

func TestClassifyError_UnknownError(t *testing.T) {
	unknownErr := errors.New("some unknown error")

	result := classifyError(unknownErr, "")
	if result == nil {
		t.Fatal("classifyError returned nil")
	}
	if result.Type != ErrorTypeUnknown {
		t.Errorf("Type = %v, want ErrorTypeUnknown", result.Type)
	}
	if result.Retryable {
		t.Error("Unknown errors should not be retryable")
	}
}

func TestRPCRequest_Fields(t *testing.T) {
	req := RPCRequest{
		Method: "get_temperature",
		Params: map[string]interface{}{
			"node": "server1",
		},
	}

	if req.Method != "get_temperature" {
		t.Errorf("Method = %q, want get_temperature", req.Method)
	}
	if req.Params["node"] != "server1" {
		t.Errorf("Params[node] = %v, want server1", req.Params["node"])
	}
}

func TestRPCResponse_Fields(t *testing.T) {
	resp := RPCResponse{
		Success: true,
		Data: map[string]interface{}{
			"temperature": "45.0",
		},
		Error: "",
	}

	if !resp.Success {
		t.Error("Success should be true")
	}
	if resp.Data["temperature"] != "45.0" {
		t.Errorf("Data[temperature] = %v, want 45.0", resp.Data["temperature"])
	}
	if resp.Error != "" {
		t.Errorf("Error = %q, want empty", resp.Error)
	}
}

func TestRPCResponse_ErrorCase(t *testing.T) {
	resp := RPCResponse{
		Success: false,
		Error:   "unauthorized",
	}

	if resp.Success {
		t.Error("Success should be false")
	}
	if resp.Error != "unauthorized" {
		t.Errorf("Error = %q, want unauthorized", resp.Error)
	}
}

func TestClient_Fields(t *testing.T) {
	client := &Client{
		socketPath: "/test/socket.sock",
		timeout:    60 * time.Second,
	}

	if client.socketPath != "/test/socket.sock" {
		t.Errorf("socketPath = %q, want /test/socket.sock", client.socketPath)
	}
	if client.timeout != 60*time.Second {
		t.Errorf("timeout = %v, want 60s", client.timeout)
	}
}

func TestProxyError_AllFields(t *testing.T) {
	wrapped := errors.New("wrapped error")
	err := ProxyError{
		Type:      ErrorTypeSSH,
		Message:   "SSH connectivity issue",
		Retryable: true,
		Wrapped:   wrapped,
	}

	if err.Type != ErrorTypeSSH {
		t.Errorf("Type = %v, want ErrorTypeSSH", err.Type)
	}
	if err.Message != "SSH connectivity issue" {
		t.Errorf("Message = %q, want 'SSH connectivity issue'", err.Message)
	}
	if !err.Retryable {
		t.Error("Retryable should be true")
	}
	if err.Wrapped != wrapped {
		t.Errorf("Wrapped = %v, want %v", err.Wrapped, wrapped)
	}
}

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

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