package http

import (
	"context"
	"errors"
	"net/http/httptest"
	"strings"
	"testing"
	"time"
)

type fakeAuthRateRedisRunner struct {
	result int64
	err    error
	script string
	keys   []string
	args   []any
}

func (f *fakeAuthRateRedisRunner) evalInt(_ context.Context, script string, keys []string, args ...any) (int64, error) {
	f.script = script
	f.keys = append([]string(nil), keys...)
	f.args = append([]any(nil), args...)
	return f.result, f.err
}

func TestRedisAuthRateLimiterUsesAtomicScriptKeysAndLimits(t *testing.T) {
	runner := &fakeAuthRateRedisRunner{result: 1}
	limiter := newRedisAuthRateLimiter(runner, AuthRateLimitConfig{Window: 2 * time.Minute})

	allowed, err := limiter.allow(context.Background(), []authRateKey{
		{key: "gateway:auth:rate:ip:203.0.113.1", limit: 3},
		{key: "", limit: 99},
		{key: "gateway:auth:rate:device:dev-1", limit: 5},
	})
	if err != nil {
		t.Fatalf("allow failed: %v", err)
	}
	if !allowed {
		t.Fatal("redis limiter should allow when script returns 1")
	}

	wantKeys := []string{"gateway:auth:rate:ip:203.0.113.1", "gateway:auth:rate:device:dev-1"}
	if len(runner.keys) != len(wantKeys) {
		t.Fatalf("unexpected redis keys: got=%v want=%v", runner.keys, wantKeys)
	}
	for i := range wantKeys {
		if runner.keys[i] != wantKeys[i] {
			t.Fatalf("unexpected redis key at %d: got=%q want=%q", i, runner.keys[i], wantKeys[i])
		}
	}
	if len(runner.args) != 3 || runner.args[0] != int64(120000) || runner.args[1] != 3 || runner.args[2] != 5 {
		t.Fatalf("unexpected redis args: %#v", runner.args)
	}
	for _, token := range []string{"GET", "INCR", "PEXPIRE"} {
		if !strings.Contains(runner.script, token) {
			t.Fatalf("redis script must contain %s: %s", token, runner.script)
		}
	}
}

func TestRedisAuthRateLimiterMapsDeniedAndErrors(t *testing.T) {
	limiter := newRedisAuthRateLimiter(&fakeAuthRateRedisRunner{result: 0}, AuthRateLimitConfig{})
	allowed, err := limiter.allow(context.Background(), []authRateKey{{key: "gateway:auth:rate:ip:203.0.113.1", limit: 1}})
	if err != nil {
		t.Fatalf("allow returned error for denied result: %v", err)
	}
	if allowed {
		t.Fatal("redis limiter should deny when script returns 0")
	}

	wantErr := errors.New("redis down")
	limiter = newRedisAuthRateLimiter(&fakeAuthRateRedisRunner{err: wantErr}, AuthRateLimitConfig{})
	allowed, err = limiter.allow(context.Background(), []authRateKey{{key: "gateway:auth:rate:ip:203.0.113.1", limit: 1}})
	if !errors.Is(err, wantErr) || allowed {
		t.Fatalf("redis error must fail closed: allowed=%v err=%v", allowed, err)
	}
}

func TestAuthRateKeysHashUntrustedComponents(t *testing.T) {
	handler := NewHandler(&fakeRoomClient{}, &fakeUserAuthClient{})
	handler.SetAuthRateLimit(AuthRateLimitConfig{KeyPrefix: "test:"})
	untrusted := strings.Repeat("x", 4096)
	request := httptest.NewRequest("POST", "/api/v1/auth/account/login", nil)
	request.Header.Set("X-Forwarded-For", untrusted)

	keys := handler.authRateKeys(request, authRateInput{
		operation:     authOperationPassword,
		deviceID:      untrusted,
		displayUserID: untrusted,
	})
	if len(keys) != 4 {
		t.Fatalf("unexpected key count: got=%d keys=%v", len(keys), keys)
	}
	if keys[0].key != "test:ip:"+authRateKeyPart(clientIP(request)) {
		t.Fatalf("ip key must use hashed component: %q", keys[0].key)
	}
	for _, key := range keys {
		if strings.Contains(key.key, untrusted) {
			t.Fatalf("rate limit key leaked untrusted raw value: %q", key.key)
		}
		if len(key.key) > 96 {
			t.Fatalf("rate limit key should remain bounded: len=%d key=%q", len(key.key), key.key)
		}
	}
}

func TestPublicAuthRateLimitDisabledSkipsBackend(t *testing.T) {
	handler := NewHandler(&fakeRoomClient{}, &fakeUserAuthClient{})
	handler.SetAuthRateLimitBackend(AuthRateLimitConfig{Enabled: false}, &fakeDenyingAuthRateLimiter{})
	request := httptest.NewRequest("POST", "/api/v1/auth/account/login", nil)

	if !handler.allowPublicAuthRequest(httptest.NewRecorder(), request, authRateInput{operation: authOperationPassword}) {
		t.Fatal("disabled auth rate limiter must skip backend")
	}
}

type fakeDenyingAuthRateLimiter struct{}

func (f *fakeDenyingAuthRateLimiter) allow(context.Context, []authRateKey) (bool, error) {
	return false, errors.New("backend should not be called")
}
