package webui

import (
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
)

// TestWebmailLoginAccountLockout confirms repeated wrong-password attempts against
// one account eventually get refused with a generic lockout message rather than
// checking the password at all, and that the lockout doesn't touch a different
// account from the same IP (the per-IP throttle, not this per-account layer, would
// apply there).
func TestWebmailLoginAccountLockout(t *testing.T) {
	app := newTestApp(t)
	app.Cfg.Section("Auth").Key("login_attempt_limit").SetValue("3")
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	victimID := createTestMailboxWithPassword(t, app, "lockout-victim@example.com", domains[0].ID, "the-real-password-1!")
	otherID := createTestMailboxWithPassword(t, app, "lockout-other@example.com", domains[0].ID, "another-password-1!")

	attempt := func(email, password string) *httptest.ResponseRecorder {
		form := url.Values{"email": {email}, "password": {password}}
		req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/login", strings.NewReader(form.Encode()))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		req.RemoteAddr = "203.0.113.9:12345"
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, req)
		return rec
	}

	for i := 0; i < 3; i++ {
		rec := attempt("lockout-victim@example.com", "wrong password")
		if !strings.Contains(rec.Body.String(), "Incorrect email or password") {
			t.Fatalf("attempt %d: expected a normal wrong-password error, got: %s", i, rec.Body.String())
		}
	}

	locked := attempt("lockout-victim@example.com", "the-real-password-1!") // even the CORRECT password now
	if !strings.Contains(locked.Body.String(), "Too many failed attempts") {
		t.Fatalf("expected the account locked out after repeated failures, got: %s", locked.Body.String())
	}
	if locked.Result().Cookies() != nil {
		for _, c := range locked.Result().Cookies() {
			if c.Name == mailboxSessionCookieName && c.Value != "" {
				t.Fatal("expected no session granted while locked out, even with the correct password")
			}
		}
	}

	// A different account from the same IP is unaffected by the per-account lockout.
	rec := attempt("lockout-other@example.com", "another-password-1!")
	found := false
	for _, c := range rec.Result().Cookies() {
		if c.Name == mailboxSessionCookieName {
			found = true
		}
	}
	if !found {
		t.Fatal("expected a different account from the same IP to log in normally")
	}
	_ = victimID
	_ = otherID
}

// TestLoginRateLimitPerIP confirms the per-IP throttle kicks in independent of which
// account is being tried, once enough requests arrive from one source IP.
func TestLoginRateLimitPerIP(t *testing.T) {
	app := newTestApp(t)
	app.loginLimiter = newIPRateLimiter(3, 1<<62) // tiny limit, effectively-infinite window for a deterministic test
	mux := app.Mux()
	domains, _ := app.DB.ListDomains()
	createTestMailboxWithPassword(t, app, "ratelimit@example.com", domains[0].ID, "correct-password-1!")

	attempt := func() *httptest.ResponseRecorder {
		form := url.Values{"email": {"ratelimit@example.com"}, "password": {"correct-password-1!"}}
		req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/login", strings.NewReader(form.Encode()))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		req.RemoteAddr = "203.0.113.10:12345"
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, req)
		return rec
	}

	for i := 0; i < 3; i++ {
		if rec := attempt(); rec.Code == http.StatusTooManyRequests {
			t.Fatalf("attempt %d: unexpectedly rate-limited early", i)
		}
	}
	if rec := attempt(); rec.Code != http.StatusTooManyRequests {
		t.Fatalf("expected 429 past the per-IP limit, got %d", rec.Code)
	}
}
