// Package feishu provides property-based tests for context cancellation handling.
package feishu

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"sync/atomic"
	"testing"
	"time"

	"AutoSSO/internal/config"
	"AutoSSO/internal/pkg/httpclient"

	"pgregory.net/rapid"
)

// TestContextCancellationPropagation tests Property 11: Context 取消传播
// **Validates: Requirements 6.2, 8.4**
//
// Property 11: Context 取消传播
// *对于任意* 带有 context 的操作，当 context 被取消时，操作应及时终止并返回取消错误。
func TestContextCancellationPropagation(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		// Generate random operation type
		operationType := rapid.IntRange(0, 4).Draw(t, "operationType")

		// Generate random delay before cancellation (1-50ms)
		cancelDelayMs := rapid.IntRange(1, 50).Draw(t, "cancelDelayMs")
		cancelDelay := time.Duration(cancelDelayMs) * time.Millisecond

		// Create a slow server that takes longer than the cancel delay
		serverDelayMs := cancelDelayMs + 100 // Server is always slower than cancel
		serverDelay := time.Duration(serverDelayMs) * time.Millisecond

		var requestReceived int32

		handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			atomic.AddInt32(&requestReceived, 1)
			// Simulate slow response
			time.Sleep(serverDelay)

			// Return appropriate response based on path
			if r.URL.Path == "/open-apis/auth/v3/tenant_access_token/internal" {
				resp := tokenResponse{
					Code:              0,
					TenantAccessToken: "test_token",
					Expire:            7200,
				}
				json.NewEncoder(w).Encode(resp)
				return
			}

			// Default response for other endpoints
			w.Header().Set("Content-Type", "application/json")
			json.NewEncoder(w).Encode(map[string]interface{}{
				"code": 0,
				"msg":  "success",
				"data": map[string]interface{}{},
			})
		})

		server := httptest.NewServer(handler)
		defer server.Close()

		cfg := &config.FeishuConfig{
			AppID:        "test_app_id",
			AppSecret:    "test_app_secret",
			ApprovalCode: "test_approval_code",
			ChatID:       "test_chat_id",
			BaseURL:      server.URL,
		}

		httpCfg := httpclient.DefaultConfig()
		httpCfg.Timeout = 5 * time.Second
		httpClient := httpclient.New(&httpCfg)
		client := NewClient(cfg, httpClient)

		// Create context that will be cancelled
		ctx, cancel := context.WithCancel(context.Background())

		// Schedule cancellation
		go func() {
			time.Sleep(cancelDelay)
			cancel()
		}()

		var err error
		startTime := time.Now()

		// Execute the operation based on type
		switch operationType {
		case 0:
			// Test GetAccessToken
			_, err = client.GetAccessToken(ctx)
		case 1:
			// Test GetNewInstances
			_, err = client.GetNewInstances(ctx, time.Now().Add(-1*time.Hour), time.Now())
		case 2:
			// Test GetInstanceDetail
			_, err = client.GetInstanceDetail(ctx, "test-instance-id")
		case 3:
			// Test SendUserNotification (will fail at token acquisition)
			err = client.SendUserNotification(ctx, "test@example.com", "Test", "Message")
		case 4:
			// Test SendGroupNotification (will fail at token acquisition)
			err = client.SendGroupNotification(ctx, "test_chat_id", "Test", "Message")
		}

		elapsed := time.Since(startTime)

		// Verify: Operation should return an error
		if err == nil {
			t.Fatalf("Operation type %d should have returned an error when context is cancelled", operationType)
		}

		// Verify: The error should be context.Canceled or contain it
		if !errors.Is(err, context.Canceled) {
			// Also accept errors that occurred due to cancellation
			// (some HTTP client implementations may wrap the error differently)
			if !isContextRelatedError(err) {
				t.Fatalf("Operation type %d should return context.Canceled error, got: %v", operationType, err)
			}
		}

		// Verify: Operation should terminate promptly after cancellation
		// Allow some tolerance for processing time
		maxExpectedDuration := cancelDelay + 100*time.Millisecond
		if elapsed > maxExpectedDuration {
			t.Fatalf("Operation type %d took too long after cancellation: %v (expected < %v)",
				operationType, elapsed, maxExpectedDuration)
		}
	})
}

// TestContextCancelledBeforeOperation tests that operations return immediately
// when context is already cancelled before the operation starts.
// **Validates: Requirements 6.2, 8.4**
func TestContextCancelledBeforeOperation(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		// Generate random operation type
		operationType := rapid.IntRange(0, 4).Draw(t, "operationType")

		var requestReceived int32

		handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			atomic.AddInt32(&requestReceived, 1)
			// This should not be reached if context is already cancelled
			resp := tokenResponse{
				Code:              0,
				TenantAccessToken: "test_token",
				Expire:            7200,
			}
			json.NewEncoder(w).Encode(resp)
		})

		server := httptest.NewServer(handler)
		defer server.Close()

		cfg := &config.FeishuConfig{
			AppID:        "test_app_id",
			AppSecret:    "test_app_secret",
			ApprovalCode: "test_approval_code",
			ChatID:       "test_chat_id",
			BaseURL:      server.URL,
		}

		httpCfg := httpclient.DefaultConfig()
		httpClient := httpclient.New(&httpCfg)
		client := NewClient(cfg, httpClient)

		// Create already-cancelled context
		ctx, cancel := context.WithCancel(context.Background())
		cancel() // Cancel immediately

		var err error
		startTime := time.Now()

		// Execute the operation based on type
		switch operationType {
		case 0:
			_, err = client.GetAccessToken(ctx)
		case 1:
			_, err = client.GetNewInstances(ctx, time.Now().Add(-1*time.Hour), time.Now())
		case 2:
			_, err = client.GetInstanceDetail(ctx, "test-instance-id")
		case 3:
			err = client.SendUserNotification(ctx, "test@example.com", "Test", "Message")
		case 4:
			err = client.SendGroupNotification(ctx, "test_chat_id", "Test", "Message")
		}

		elapsed := time.Since(startTime)

		// Verify: Operation should return an error
		if err == nil {
			t.Fatalf("Operation type %d should have returned an error with pre-cancelled context", operationType)
		}

		// Verify: The error should be context.Canceled
		if !errors.Is(err, context.Canceled) && !isContextRelatedError(err) {
			t.Fatalf("Operation type %d should return context.Canceled error, got: %v", operationType, err)
		}

		// Verify: Operation should return almost immediately (< 50ms)
		if elapsed > 50*time.Millisecond {
			t.Fatalf("Operation type %d should return immediately with pre-cancelled context, took: %v",
				operationType, elapsed)
		}
	})
}

// TestContextDeadlineExceeded tests that operations return DeadlineExceeded
// when context deadline is exceeded.
// **Validates: Requirements 6.2, 8.4**
func TestContextDeadlineExceeded(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		// Generate random operation type
		operationType := rapid.IntRange(0, 4).Draw(t, "operationType")

		// Generate random timeout (10-50ms)
		timeoutMs := rapid.IntRange(10, 50).Draw(t, "timeoutMs")
		timeout := time.Duration(timeoutMs) * time.Millisecond

		// Server delay is always longer than timeout
		serverDelay := timeout + 200*time.Millisecond

		handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// Simulate slow response
			time.Sleep(serverDelay)

			resp := tokenResponse{
				Code:              0,
				TenantAccessToken: "test_token",
				Expire:            7200,
			}
			json.NewEncoder(w).Encode(resp)
		})

		server := httptest.NewServer(handler)
		defer server.Close()

		cfg := &config.FeishuConfig{
			AppID:        "test_app_id",
			AppSecret:    "test_app_secret",
			ApprovalCode: "test_approval_code",
			ChatID:       "test_chat_id",
			BaseURL:      server.URL,
		}

		httpCfg := httpclient.DefaultConfig()
		httpCfg.Timeout = 5 * time.Second // HTTP client timeout is longer
		httpClient := httpclient.New(&httpCfg)
		client := NewClient(cfg, httpClient)

		// Create context with deadline
		ctx, cancel := context.WithTimeout(context.Background(), timeout)
		defer cancel()

		var err error
		startTime := time.Now()

		// Execute the operation based on type
		switch operationType {
		case 0:
			_, err = client.GetAccessToken(ctx)
		case 1:
			_, err = client.GetNewInstances(ctx, time.Now().Add(-1*time.Hour), time.Now())
		case 2:
			_, err = client.GetInstanceDetail(ctx, "test-instance-id")
		case 3:
			err = client.SendUserNotification(ctx, "test@example.com", "Test", "Message")
		case 4:
			err = client.SendGroupNotification(ctx, "test_chat_id", "Test", "Message")
		}

		elapsed := time.Since(startTime)

		// Verify: Operation should return an error
		if err == nil {
			t.Fatalf("Operation type %d should have returned an error when deadline exceeded", operationType)
		}

		// Verify: The error should be context.DeadlineExceeded or context-related
		if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, context.Canceled) && !isContextRelatedError(err) {
			t.Fatalf("Operation type %d should return context.DeadlineExceeded error, got: %v", operationType, err)
		}

		// Verify: Operation should terminate around the timeout
		// Allow some tolerance for processing time
		maxExpectedDuration := timeout + 100*time.Millisecond
		if elapsed > maxExpectedDuration {
			t.Fatalf("Operation type %d took too long after deadline: %v (expected < %v)",
				operationType, elapsed, maxExpectedDuration)
		}
	})
}

// TestContextCancellationDuringRetry tests that context cancellation stops retry attempts.
// **Validates: Requirements 6.2, 8.4**
func TestContextCancellationDuringRetry(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		// Generate random number of retries before cancellation
		cancelAfterAttempts := rapid.IntRange(1, 3).Draw(t, "cancelAfterAttempts")

		var attemptCount int32
		var cancelFunc context.CancelFunc

		handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			count := atomic.AddInt32(&attemptCount, 1)

			// Cancel context after specified number of attempts
			if int(count) >= cancelAfterAttempts && cancelFunc != nil {
				cancelFunc()
			}

			// Always return error to trigger retry
			w.WriteHeader(http.StatusInternalServerError)
			json.NewEncoder(w).Encode(map[string]interface{}{
				"code": 500,
				"msg":  "internal error",
			})
		})

		server := httptest.NewServer(handler)
		defer server.Close()

		cfg := &config.FeishuConfig{
			AppID:        "test_app_id",
			AppSecret:    "test_app_secret",
			ApprovalCode: "test_approval_code",
			ChatID:       "test_chat_id",
			BaseURL:      server.URL,
		}

		httpCfg := httpclient.DefaultConfig()
		httpCfg.Timeout = 5 * time.Second
		httpClient := httpclient.New(&httpCfg)
		client := NewClient(cfg, httpClient)

		// Create cancellable context
		ctx, cancel := context.WithCancel(context.Background())
		cancelFunc = cancel
		defer cancel()

		// Try to get access token (will retry on failure)
		_, err := client.GetAccessToken(ctx)

		// Verify: Operation should return an error
		if err == nil {
			t.Fatal("GetAccessToken should have returned an error")
		}

		// Verify: Should have stopped retrying after cancellation
		// The retry mechanism should respect context cancellation
		finalCount := atomic.LoadInt32(&attemptCount)
		maxExpectedAttempts := int32(cancelAfterAttempts + 2) // Allow some tolerance for in-flight requests

		if finalCount > maxExpectedAttempts {
			t.Fatalf("Expected at most %d attempts after cancellation, got %d",
				maxExpectedAttempts, finalCount)
		}
	})
}

// TestErrorIdentifiableViaErrorsIs tests that context errors are identifiable
// via errors.Is() function.
// **Validates: Requirements 6.2, 8.4**
func TestErrorIdentifiableViaErrorsIs(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		// Generate error type: 0 = Canceled, 1 = DeadlineExceeded
		errorType := rapid.IntRange(0, 1).Draw(t, "errorType")

		handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// Slow response to ensure context expires
			time.Sleep(500 * time.Millisecond)
			json.NewEncoder(w).Encode(tokenResponse{
				Code:              0,
				TenantAccessToken: "test_token",
				Expire:            7200,
			})
		})

		server := httptest.NewServer(handler)
		defer server.Close()

		cfg := &config.FeishuConfig{
			AppID:        "test_app_id",
			AppSecret:    "test_app_secret",
			ApprovalCode: "test_approval_code",
			ChatID:       "test_chat_id",
			BaseURL:      server.URL,
		}

		httpCfg := httpclient.DefaultConfig()
		httpCfg.Timeout = 5 * time.Second
		httpClient := httpclient.New(&httpCfg)
		client := NewClient(cfg, httpClient)

		var ctx context.Context
		var cancel context.CancelFunc
		var expectedError error

		switch errorType {
		case 0:
			// Test context.Canceled
			ctx, cancel = context.WithCancel(context.Background())
			cancel() // Cancel immediately
			expectedError = context.Canceled
		case 1:
			// Test context.DeadlineExceeded
			ctx, cancel = context.WithTimeout(context.Background(), 10*time.Millisecond)
			defer cancel()
			expectedError = context.DeadlineExceeded
		}

		_, err := client.GetAccessToken(ctx)

		// Verify: Operation should return an error
		if err == nil {
			t.Fatalf("GetAccessToken should have returned an error for error type %d", errorType)
		}

		// Verify: The error should be identifiable via errors.Is
		if !errors.Is(err, expectedError) && !isContextRelatedError(err) {
			t.Fatalf("Error should be identifiable via errors.Is(%v), got: %v", expectedError, err)
		}
	})
}

// TestAllOperationsRespectContext tests that all client operations respect context.
// **Validates: Requirements 6.2, 8.4**
func TestAllOperationsRespectContext(t *testing.T) {
	operations := []struct {
		name string
		fn   func(ctx context.Context, client *Client) error
	}{
		{
			name: "GetAccessToken",
			fn: func(ctx context.Context, client *Client) error {
				_, err := client.GetAccessToken(ctx)
				return err
			},
		},
		{
			name: "GetNewInstances",
			fn: func(ctx context.Context, client *Client) error {
				_, err := client.GetNewInstances(ctx, time.Now().Add(-1*time.Hour), time.Now())
				return err
			},
		},
		{
			name: "GetInstanceDetail",
			fn: func(ctx context.Context, client *Client) error {
				_, err := client.GetInstanceDetail(ctx, "test-instance")
				return err
			},
		},
		{
			name: "SendUserNotification",
			fn: func(ctx context.Context, client *Client) error {
				return client.SendUserNotification(ctx, "test@example.com", "Test", "Message")
			},
		},
		{
			name: "SendGroupNotification",
			fn: func(ctx context.Context, client *Client) error {
				return client.SendGroupNotification(ctx, "test_chat_id", "Test", "Message")
			},
		},
	}

	for _, op := range operations {
		t.Run(op.name, func(t *testing.T) {
			rapid.Check(t, func(t *rapid.T) {
				handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					// Slow response
					time.Sleep(500 * time.Millisecond)
					json.NewEncoder(w).Encode(tokenResponse{
						Code:              0,
						TenantAccessToken: "test_token",
						Expire:            7200,
					})
				})

				server := httptest.NewServer(handler)
				defer server.Close()

				cfg := &config.FeishuConfig{
					AppID:        "test_app_id",
					AppSecret:    "test_app_secret",
					ApprovalCode: "test_approval_code",
					ChatID:       "test_chat_id",
					BaseURL:      server.URL,
				}

				httpCfg := httpclient.DefaultConfig()
				httpCfg.Timeout = 5 * time.Second
				httpClient := httpclient.New(&httpCfg)
				client := NewClient(cfg, httpClient)

				// Create already-cancelled context
				ctx, cancel := context.WithCancel(context.Background())
				cancel()

				err := op.fn(ctx, client)

				// Verify: Operation should return an error
				if err == nil {
					t.Fatalf("%s should have returned an error with cancelled context", op.name)
				}

				// Verify: Error should be context-related
				if !errors.Is(err, context.Canceled) && !isContextRelatedError(err) {
					t.Fatalf("%s should return context.Canceled error, got: %v", op.name, err)
				}
			})
		})
	}
}

// isContextRelatedError checks if an error is related to context cancellation.
// This helper handles cases where the error might be wrapped differently.
func isContextRelatedError(err error) bool {
	if err == nil {
		return false
	}

	// Check for standard context errors
	if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
		return true
	}

	// Check error message for context-related keywords
	errStr := err.Error()
	contextKeywords := []string{
		"context canceled",
		"context deadline exceeded",
		"operation was canceled",
		"request canceled",
		"context cancelled",
	}

	for _, keyword := range contextKeywords {
		if containsIgnoreCase(errStr, keyword) {
			return true
		}
	}

	return false
}

// containsIgnoreCase checks if s contains substr (case-insensitive).
func containsIgnoreCase(s, substr string) bool {
	return len(s) >= len(substr) && (s == substr ||
		len(s) > 0 && len(substr) > 0 &&
			(s[0] == substr[0] || s[0]+32 == substr[0] || s[0] == substr[0]+32) &&
			containsIgnoreCase(s[1:], substr[1:]) ||
		len(s) > 0 && containsIgnoreCase(s[1:], substr))
}
