package backendclient_test

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

	"scrabble/gateway/internal/backendclient"
	"scrabble/gateway/internal/ratelimit"
)

// TestReportRateLimited verifies the rejection report reaches the backend's
// internal endpoint with the agreed JSON shape and no user identity.
func TestReportRateLimited(t *testing.T) {
	var got struct {
		WindowSeconds int                   `json:"window_seconds"`
		Entries       []ratelimit.Rejection `json:"entries"`
	}
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost || r.URL.Path != "/api/v1/internal/ratelimit/report" {
			t.Errorf("call = %s %s, want POST /api/v1/internal/ratelimit/report", r.Method, r.URL.Path)
		}
		if uid := r.Header.Get("X-User-ID"); uid != "" {
			t.Errorf("X-User-ID = %q, want empty", uid)
		}
		if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
			t.Errorf("decode report: %v", err)
		}
	}))
	defer srv.Close()

	c, err := backendclient.New(srv.URL, "localhost:9090", 2*time.Second)
	if err != nil {
		t.Fatalf("backendclient: %v", err)
	}
	defer func() { _ = c.Close() }()

	entries := []ratelimit.Rejection{{Class: "user", Key: "u-1", Rejected: 5}}
	if err := c.ReportRateLimited(context.Background(), 30, entries); err != nil {
		t.Fatalf("ReportRateLimited: %v", err)
	}
	if got.WindowSeconds != 30 || len(got.Entries) != 1 || got.Entries[0] != entries[0] {
		t.Fatalf("backend received %+v, want window 30 + %+v", got, entries[0])
	}
}

// TestSyncBans verifies the gateway reports its active bans to the backend's
// internal endpoint and returns the operator unban list the backend replies with.
func TestSyncBans(t *testing.T) {
	var got struct {
		Active []ratelimit.Ban `json:"active"`
	}
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost || r.URL.Path != "/api/v1/internal/bans/sync" {
			t.Errorf("call = %s %s, want POST /api/v1/internal/bans/sync", r.Method, r.URL.Path)
		}
		if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
			t.Errorf("decode sync: %v", err)
		}
		_, _ = w.Write([]byte(`{"unban":["203.0.113.9"]}`))
	}))
	defer srv.Close()

	c, err := backendclient.New(srv.URL, "localhost:9090", 2*time.Second)
	if err != nil {
		t.Fatalf("backendclient: %v", err)
	}
	defer func() { _ = c.Close() }()

	active := []ratelimit.Ban{{IP: "198.51.100.4", Reason: ratelimit.ReasonTripwire}}
	unban, err := c.SyncBans(context.Background(), active)
	if err != nil {
		t.Fatalf("SyncBans: %v", err)
	}
	if len(got.Active) != 1 || got.Active[0].IP != "198.51.100.4" || got.Active[0].Reason != ratelimit.ReasonTripwire {
		t.Fatalf("backend received active = %+v, want one tripwire ban for 198.51.100.4", got.Active)
	}
	if len(unban) != 1 || unban[0] != "203.0.113.9" {
		t.Fatalf("unban = %v, want [203.0.113.9]", unban)
	}
}

// TestXPlatformInjection verifies the trusted platform carried on the context
// (WithPlatform) rides an authenticated backend request as X-Platform, and that an
// untrusted context (no platform) sends no header at all — the fail-closed default.
func TestXPlatformInjection(t *testing.T) {
	var gotPlatform string
	var hadHeader bool
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		gotPlatform = r.Header.Get("X-Platform")
		_, hadHeader = r.Header["X-Platform"]
		_, _ = w.Write([]byte(`{}`))
	}))
	defer srv.Close()

	c, err := backendclient.New(srv.URL, "localhost:9090", 2*time.Second)
	if err != nil {
		t.Fatalf("backendclient: %v", err)
	}
	defer func() { _ = c.Close() }()

	t.Run("trusted forwards header", func(t *testing.T) {
		ctx := backendclient.WithPlatform(context.Background(), "vk/ios")
		if _, err := c.Profile(ctx, "user-1"); err != nil {
			t.Fatalf("Profile: %v", err)
		}
		if gotPlatform != "vk/ios" {
			t.Fatalf("X-Platform = %q, want vk/ios", gotPlatform)
		}
	})

	t.Run("untrusted omits header", func(t *testing.T) {
		hadHeader = true // ensure the handler actually clears it
		if _, err := c.Profile(context.Background(), "user-1"); err != nil {
			t.Fatalf("Profile: %v", err)
		}
		if hadHeader {
			t.Fatal("X-Platform must be absent for an untrusted context")
		}
	})
}

// TestXForwardedForInjection verifies the client IP carried on the context (WithClientIP) rides every
// backend call as X-Forwarded-For — not just chat/feedback, which pass it explicitly — so the backend
// records the real caller (e.g. the account's last-login IP on the profile fetch) rather than the
// gateway's own connection. Absent when no client IP is set.
func TestXForwardedForInjection(t *testing.T) {
	var gotXFF string
	var hadHeader bool
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		gotXFF = r.Header.Get("X-Forwarded-For")
		_, hadHeader = r.Header["X-Forwarded-For"]
		_, _ = w.Write([]byte(`{}`))
	}))
	defer srv.Close()

	c, err := backendclient.New(srv.URL, "localhost:9090", 2*time.Second)
	if err != nil {
		t.Fatalf("backendclient: %v", err)
	}
	defer func() { _ = c.Close() }()

	t.Run("client IP on ctx rides a non-chat call", func(t *testing.T) {
		ctx := backendclient.WithClientIP(context.Background(), "203.0.113.7")
		if _, err := c.Profile(ctx, "user-1"); err != nil {
			t.Fatalf("Profile: %v", err)
		}
		if gotXFF != "203.0.113.7" {
			t.Fatalf("X-Forwarded-For = %q, want 203.0.113.7", gotXFF)
		}
	})

	t.Run("no client IP omits the header", func(t *testing.T) {
		hadHeader = true
		if _, err := c.Profile(context.Background(), "user-1"); err != nil {
			t.Fatalf("Profile: %v", err)
		}
		if hadHeader {
			t.Fatal("X-Forwarded-For must be absent when no client IP is set")
		}
	})
}
