package yandex

import (
	"encoding/json"
	"fmt"
	"strings"
	"testing"
	"time"
)

func TestTokenBucket(t *testing.T) {
	// Test token bucket with capacity 5, refill 1 per second
	tb := newTokenBucket(5, 1) // 1 token per second

	// Should immediately acquire tokens
	for i := 0; i < 5; i++ {
		if err := tb.acquire(); err != nil {
			t.Fatalf("expected no error, got: %v", err)
		}
	}

	// 6th acquire should fail (rate limited)
	if err := tb.acquire(); err == nil {
		t.Error("expected rate limit error on 6th acquire, got nil")
	}

	// Wait for refill and should succeed
	time.Sleep(1*time.Second + 10*time.Millisecond)
	if err := tb.acquire(); err != nil {
		t.Fatalf("expected to acquire after refill, got: %v", err)
	}
}

func TestTokenBucketCapacity(t *testing.T) {
	tb := newTokenBucket(3, 10) // 10 TPS, capacity 3

	// Should start with 3 tokens
	if err := tb.acquire(); err != nil {
		t.Fatalf("expected success on first acquire, got: %v", err)
	}
	if err := tb.acquire(); err != nil {
		t.Fatalf("expected success on second acquire, got: %v", err)
	}
	if err := tb.acquire(); err != nil {
		t.Fatalf("expected success on third acquire, got: %v", err)
	}

	// 4th should fail
	if err := tb.acquire(); err == nil {
		t.Error("expected rate limit error on 4th acquire")
	}

	// Wait partial refill - should have some tokens back
	time.Sleep(500 * time.Millisecond)
	// May or may not have a token depending on refill math, but shouldn't panic
	_ = tb.acquire()
}

func TestCircuitBreakerClosed(t *testing.T) {
	cb := newCircuitBreaker()

	// Initially should be closed and allow requests
	for i := 0; i < 10; i++ {
		if !cb.allow() {
			t.Fatalf("expected circuit breaker to be closed and allow request %d", i)
		}
	}
}

func TestCircuitBreakerOpenAfterFailures(t *testing.T) {
	cb := newCircuitBreaker()

	// Record 3 failures to open the circuit (failThreshold = 3)
	for i := 0; i < 3; i++ {
		cb.recordFailure()
	}

	// Should now be open - allow() should return false (circuit open, requests rejected)
	if cb.allow() {
		t.Error("expected allow() to return false (circuit open), got true")
	}

	// Should have recorded the state transition
	if cb.state != open {
		t.Errorf("expected state open, got %v", cb.state)
	}

	// Wait for timeout
	time.Sleep(31 * time.Second)

	// Should transition to half-open/open after timeout - allow() should return true
	if !cb.allow() {
		t.Error("expected allow() to return true after timeout")
	}
}

func TestCircuitBreakerRecordSuccess(t *testing.T) {
	cb := newCircuitBreaker()

	// Record 5 failures to open
	for i := 0; i < 5; i++ {
		cb.recordFailure()
	}

	if cb.state != open {
		t.Errorf("expected state open after 5 failures, got %v", cb.state)
	}

	// Record 3 successes in half-open state
	// First need to transition to half-open by waiting timeout,
	// but let's just test the success recording directly
	// by manually setting state
	cb.state = halfOpen
	cb.successes = 0

	for i := 0; i < 3; i++ {
		cb.recordSuccess()
	}

	if cb.state != closed {
		t.Errorf("expected state closed after 3 successes from half-open, got %v", cb.state)
	}
}

func TestCircuitBreakerRecordFailureFromClosed(t *testing.T) {
	cb := newCircuitBreaker()

	// Record failures
	cb.recordFailure()
	cb.recordFailure()

	if cb.state != closed {
		t.Errorf("expected still closed after 2 failures, got %v", cb.state)
	}

	// 3rd failure should open
	cb.recordFailure()

	if cb.state != open {
		t.Errorf("expected open after 3rd failure, got %v", cb.state)
	}
}

func TestCircuitBreakerRecordSuccessFromHalfOpen(t *testing.T) {
	cb := newCircuitBreaker()

	// Simulate: 2 failures open the circuit, then 3 successes close it
	cb.recordFailure()
	cb.recordFailure() // state = open

	// Wait enough time to transition to half-open
	// (in real usage would wait the timeout duration)
	cb.state = halfOpen
	cb.successes = 0

	cb.recordSuccess()
	cb.recordSuccess()
	cb.recordSuccess()

	if cb.state != closed {
		t.Errorf("expected closed after 3 successes from half-open, got %v", cb.state)
	}
}

func TestRetrySuccessAfterBackoff(t *testing.T) {
	// This tests the retry logic with a mock that fails then succeeds
	// We test the retry config and backoff timing
	cfg := &retryConfig{
		maxRetries:  3,
		baseBackoff: 50 * time.Millisecond,
		maxBackoff:  2 * time.Second,
		jitter:      false,
	}

	// Verify backoff sequence
	backoffs := []time.Duration{}
	for i := 0; i < cfg.maxRetries; i++ {
		backoff := cfg.baseBackoff
		if cfg.jitter {
			backoff = applyJitter(backoff)
		}
		backoffs = append(backoffs, backoff)
	}

	// With jitter=false, all should be 50ms
	for i, b := range backoffs {
		expected := 50 * time.Millisecond
		if b != expected {
			t.Errorf("backoff %d: expected %v, got %v", i, expected, b)
		}
	}
}

func TestRetryExhaustion(t *testing.T) {
	cfg := &retryConfig{
		maxRetries:  2,
		baseBackoff: 10 * time.Millisecond,
		maxBackoff:  1 * time.Second,
		jitter:      false,
	}

	// Simulate consecutive failures
	var lastErr error
	for attempt := 0; attempt <= cfg.maxRetries; attempt++ {
		// Simulate a non-retryable error that gets recorded as failure
		// In real code, isRetryableError would return false
		lastErr = fmt.Errorf("attempt %d failed", attempt)
		_ = lastErr // track last error
	}

	// After maxRetries+1 attempts (0-indexed: 0 to maxRetries), we've done 3 attempts
	// with 2 retries (attempts 0->1, 1->2), the 3rd attempt (index 2) is the last
	if cfg.maxRetries+1 < 3 {
		t.Error("expected at least 3 attempts with maxRetries=2")
	}
}

// Test that API client integrates rate limiter + circuit breaker + retry
func TestClientDoIntegration(t *testing.T) {
	c := NewClient("test-api-key")

	// Verify defaults are set
	if c.rateLimiter == nil {
		t.Error("expected rate limiter to be initialized")
	}
	if c.circuitBreaker == nil {
		t.Error("expected circuit breaker to be initialized")
	}
	if c.retryConfig == nil {
		t.Error("expected retry config to be initialized")
	}

	// Verify rate limiter settings
	if c.rateLimiter.capacity != 10 {
		t.Errorf("expected rate limiter capacity 10, got %d", c.rateLimiter.capacity)
	}
	if c.retryConfig.maxRetries != 3 {
		t.Errorf("expected max retries 3, got %d", c.retryConfig.maxRetries)
	}

	// Test with custom options
	custom := NewClient("custom-key",
		WithRateLimiter(5, 2), // 5 TPS
		WithCircuitBreakerTimeout(10*time.Second),
		WithRetryConfig(5, 200*time.Millisecond, 10*time.Second, false))

	if custom.rateLimiter.capacity != 5 {
		t.Errorf("expected custom rate limiter capacity 5, got %d", custom.rateLimiter.capacity)
	}
	if custom.circuitBreaker.timeout != 10*time.Second {
		t.Errorf("expected custom circuit breaker timeout 10s, got %v", custom.circuitBreaker.timeout)
	}
	if custom.retryConfig.maxRetries != 5 {
		t.Errorf("expected custom max retries 5, got %d", custom.retryConfig.maxRetries)
	}
	if custom.retryConfig.baseBackoff != 200*time.Millisecond {
		t.Errorf("expected custom base backoff 200ms, got %v", custom.retryConfig.baseBackoff)
	}
}

// Test building a Yandex API URL
func TestBuildURL(t *testing.T) {
	query := map[string]string{
		"from": "c146",
		"to":   "c213",
		"date": "2026-08-15",
	}

	url := buildURL("/v3.0/search/", query)
	if url == "" {
		t.Error("expected non-empty URL")
	}
	if !strings.Contains(url, "from=c146") {
		t.Errorf("expected URL to contain from=c146, got %s", url)
	}
	if !strings.Contains(url, "to=c213") {
		t.Errorf("expected URL to contain to=c213, got %s", url)
	}
	if !strings.Contains(url, "date=2026-08-15") {
		t.Errorf("expected URL to contain date=2026-08-15, got %s", url)
	}
}

// Test API error creation
func TestAPIError(t *testing.T) {
	err := newAPIError(404, "Not Found")
	if err == nil {
		t.Error("expected non-nil APIError")
	}
	expected := "API error 404: Not Found"
	if err.Error() != expected {
		t.Errorf("expected '%s', got '%s'", expected, err.Error())
	}
}

// Test isRetryableError
func TestIsRetryableError(t *testing.T) {
	// Network errors are retryable
	err := fmt.Errorf("connection timeout")
	if !isRetryableError(err) {
		t.Error("expected network error to be retryable")
	}

	// Nil is not retryable
	if isRetryableError(nil) {
		t.Error("expected nil error to not be retryable")
	}
}

func TestResetCircuitBreaker(t *testing.T) {
	cb := newCircuitBreaker()

	// Record failures to open the circuit
	cb.recordFailure()
	cb.recordFailure()
	cb.recordFailure()

	if cb.state != open {
		t.Errorf("expected state open after 3 failures, got %v", cb.state)
	}

	// Reset the circuit breaker
	cb.ResetCircuitBreaker()

	// Should be back to closed state
	if cb.state != closed {
		t.Errorf("expected state closed after reset, got %v", cb.state)
	}
	if cb.failures != 0 {
		t.Errorf("expected failures to be 0 after reset, got %d", cb.failures)
	}
	if cb.successes != 0 {
		t.Errorf("expected successes to be 0 after reset, got %d", cb.successes)
	}
	if cb.openSince != (time.Time{}) {
		t.Errorf("expected openSince to be zero after reset, got %v", cb.openSince)
	}

	// After reset, allow() should return true (circuit closed)
	for i := 0; i < 10; i++ {
		if !cb.allow() {
			t.Fatalf("expected allow() to return true after reset, attempt %d", i)
		}
	}
}

// Test Response parsing
func TestResponseParsing(t *testing.T) {
	// Test with a valid JSON response
	jsonData := `{
		"pagination": {"total": 5, "limit": 100, "offset": 0},
		"search": {"date": "2026-08-13", "from": {"code": "c146", "type": "settlement", "title": "Simferopol"}, "to": {"code": "c213", "type": "settlement", "title": "Moscow"}},
		"interval_segments": [],
		"segments": []
	}`

	var resp Response
	if err := json.Unmarshal([]byte(jsonData), &resp); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}

	if resp.Pagination.Total != 5 {
		t.Errorf("expected total 5, got %d", resp.Pagination.Total)
	}
	expectedFrom := "Simferopol"
	if resp.Search.From.Title != expectedFrom {
		t.Errorf("expected from title %s, got %s", expectedFrom, resp.Search.From.Title)
	}
	expectedTo := "Moscow"
	if resp.Search.To.Title != expectedTo {
		t.Errorf("expected to title %s, got %s", expectedTo, resp.Search.To.Title)
	}
}

// Test Segment parsing
func TestSegmentParsing(t *testing.T) {
	jsonData := `{
		"departure": "2026-08-13T08:00:00+03:00",
		"arrival": "2026-08-13T14:00:00+03:00",
		"duration": 21600,
		"has_transfers": false,
		"from": {"code": "s9600213", "title": "Шереметьево", "transport_type": "plane"},
		"to": {"code": "s9600396", "title": "Симферополь", "transport_type": "plane"}
	}`

	var seg Segment
	if err := json.Unmarshal([]byte(jsonData), &seg); err != nil {
		t.Fatalf("failed to unmarshal segment: %v", err)
	}

	if seg.Duration != 21600 {
		t.Errorf("expected duration 21600, got %d", seg.Duration)
	}
	if seg.HasTransfers != false {
		t.Errorf("expected has_transfers false, got %v", seg.HasTransfers)
	}
	expectedFrom := "Шереметьево"
	if seg.From.Title != expectedFrom {
		t.Errorf("expected from title %s, got %s", expectedFrom, seg.From.Title)
	}
	expectedTo := "Симферополь"
	if seg.To.Title != expectedTo {
		t.Errorf("expected to title %s, got %s", expectedTo, seg.To.Title)
	}
}
