package web

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

	"gitea.dooplex.hu/admin/felhom-controller/internal/config"
	"golang.org/x/crypto/bcrypt"
)

// rateLimitTestServer builds a minimal Server with a real password hash and an initialized
// loginAttempts map, templates loaded (renderLogin needs s.tmpl). No agent/backup/settings — the
// re-auth rate limiter fires before any of those.
func rateLimitTestServer(t *testing.T) *Server {
	t.Helper()
	cfg := &config.Config{}
	hash, _ := bcrypt.GenerateFromPassword([]byte("correct-pass"), bcrypt.MinCost)
	cfg.Web.PasswordHash = string(hash)
	s := &Server{cfg: cfg, logger: log.New(io.Discard, "", 0), version: "test",
		loginAttempts: map[string]*loginAttempt{}, sessions: map[string]*session{}}
	s.loadTemplates()
	return s
}

func doLogin(s *Server, remoteAddr, xff, password string) *httptest.ResponseRecorder {
	form := url.Values{"password": {password}}
	r := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
	r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	r.RemoteAddr = remoteAddr
	if xff != "" {
		r.Header.Set("X-Forwarded-For", xff)
	}
	w := httptest.NewRecorder()
	s.handleLogin(w, r)
	return w
}

func ex(s string) string {
	if len(s) > 200 {
		return s[:200]
	}
	return s
}

// Scenario A (the F-B fix): 6 failed logins from ONE host over DISTINCT ephemeral ports and NO
// X-Forwarded-For must share a rate-limit key and trip the limiter on attempt 6. Pre-fix (raw
// RemoteAddr key) each port is its own key → never limited. This is the F-B red-proof anchor.
func TestLoginRateLimit_DirectDistinctPorts_Limited(t *testing.T) {
	s := rateLimitTestServer(t)
	var last string
	for i := 1; i <= 6; i++ {
		last = doLogin(s, fmt.Sprintf("127.0.0.1:%d", 5000+i), "", "wrong").Body.String()
		if i < 6 && !strings.Contains(last, "Hibás jelszó") {
			t.Fatalf("attempt %d expected Hibás jelszó, got: %s", i, ex(last))
		}
	}
	if !strings.Contains(last, "Túl sok sikertelen") {
		t.Fatalf("attempt 6 (distinct ports, no XFF) MUST be rate-limited; got: %s", ex(last))
	}
}

// Scenario B (regression): stable X-Forwarded-For across 6 attempts still limits on 6.
func TestLoginRateLimit_StableXFF_Limited(t *testing.T) {
	s := rateLimitTestServer(t)
	var last string
	for i := 1; i <= 6; i++ {
		last = doLogin(s, fmt.Sprintf("10.9.9.9:%d", 5000+i), "203.0.113.9", "wrong").Body.String()
	}
	if !strings.Contains(last, "Túl sok sikertelen") {
		t.Fatalf("attempt 6 with a stable XFF MUST be rate-limited; got: %s", ex(last))
	}
}

// Scenario C (documented accepted limitation): rotating the X-Forwarded-For first hop evades the
// per-IP counter. This is NOT what the fix targets (XFF is attacker-controlled on a direct path);
// the test pins the known behavior so a future XFF-trust change is a conscious decision.
func TestLoginRateLimit_RotatingXFF_NotLimited(t *testing.T) {
	s := rateLimitTestServer(t)
	var last string
	for i := 1; i <= 6; i++ {
		last = doLogin(s, "10.9.9.9:5000", fmt.Sprintf("203.0.113.%d", i), "wrong").Body.String()
	}
	if strings.Contains(last, "Túl sok sikertelen") {
		t.Fatalf("rotating XFF is a known evasion (out of scope) — expected NOT limited")
	}
	if !strings.Contains(last, "Hibás jelszó") {
		t.Fatalf("expected Hibás jelszó on rotating XFF; got: %s", ex(last))
	}
}

// Scenario D: the escrow wizard re-auth path shares the SAME fixed key (proves the shared clientIP
// helper reached escrow_handlers, not just handleLogin). 6 wrong wizard passwords over distinct
// ports → 429 on attempt 6.
func TestEscrowReauthRateLimit_SharesFixedKey(t *testing.T) {
	s := rateLimitTestServer(t)
	var code int
	for i := 1; i <= 6; i++ {
		form := url.Values{"password": {"wrong"}}
		r := httptest.NewRequest(http.MethodPost, "/api/escrow/start", strings.NewReader(form.Encode()))
		r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		r.RemoteAddr = fmt.Sprintf("127.0.0.1:%d", 6000+i)
		w := httptest.NewRecorder()
		s.escrowStartAPIHandler(w, r)
		code = w.Code
	}
	if code != http.StatusTooManyRequests {
		t.Fatalf("escrow re-auth attempt 6 (distinct ports, no XFF) MUST be 429; got %d", code)
	}
}

// Scenario E: a successful login clears the counter for that host key.
func TestLoginRateLimit_SuccessClearsCounter(t *testing.T) {
	s := rateLimitTestServer(t)
	for i := 1; i <= 4; i++ {
		doLogin(s, fmt.Sprintf("127.0.0.1:%d", 7000+i), "", "wrong")
	}
	if s.loginAttempts["127.0.0.1"] == nil || s.loginAttempts["127.0.0.1"].count != 4 {
		t.Fatalf("expected 4 accrued failures on the shared key before success")
	}
	doLogin(s, "127.0.0.1:7099", "", "correct-pass") // success
	if s.loginAttempts["127.0.0.1"] != nil {
		t.Fatalf("a successful login must clear the host's failure counter")
	}
}

// clientIP unit: port stripped; XFF first-hop wins; no-port and IPv6 handled.
func TestClientIP_StripsPort(t *testing.T) {
	cases := []struct{ remote, xff, want string }{
		{"127.0.0.1:5001", "", "127.0.0.1"},
		{"127.0.0.1:5002", "203.0.113.9", "203.0.113.9"},
		{"[::1]:443", "", "::1"},
		{"192.168.0.5", "", "192.168.0.5"}, // no port → raw
		{"10.0.0.1:80", "198.51.100.7, 203.0.113.9", "198.51.100.7"},
	}
	for _, c := range cases {
		r := httptest.NewRequest(http.MethodGet, "/", nil)
		r.RemoteAddr = c.remote
		if c.xff != "" {
			r.Header.Set("X-Forwarded-For", c.xff)
		}
		if got := clientIP(r); got != c.want {
			t.Errorf("clientIP(remote=%q xff=%q) = %q, want %q", c.remote, c.xff, got, c.want)
		}
	}
}
