// Tests for the M13a W5 JWT gate in archiverd. We test the
// gate wiring + the trigger-channel semantics. We don't spin
// up the runLoop (no Postgres / ClickHouse in unit tests); the
// admin handler is decoupled from the loop via the channel.
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"os"
	"testing"
	"time"
)

const testSecret = "test-secret-with-32-bytes-min-len-abc"

func mintTestJWT(t *testing.T, secret, role string) string {
	t.Helper()
	hb, _ := json.Marshal(map[string]string{"alg": "HS256", "typ": "JWT"})
	body, _ := json.Marshal(map[string]any{
		"sub":  "u-test",
		"tid":  "t-test",
		"role": role,
		"typ":  "access",
		"iss":  "broad-announce",
		"exp":  time.Now().Add(15 * time.Minute).Unix(),
		"iat":  time.Now().Unix(),
	})
	enc := base64.RawURLEncoding.EncodeToString(hb) + "." + base64.RawURLEncoding.EncodeToString(body)
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(enc))
	return enc + "." + base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
}

func discardLogger() *slog.Logger {
	return slog.New(slog.NewTextHandler(io.Discard, nil))
}

// TestWireAdminRoutes_NoSecret_Disabled verifies that with
// BA_AUTHD_JWT_SECRET unset, the admin route is NOT registered.
func TestWireAdminRoutes_NoSecret_Disabled(t *testing.T) {
	os.Unsetenv("BA_AUTHD_JWT_SECRET")
	mux := http.NewServeMux()
	triggerCh := make(chan struct{}, 1)
	wireAdminRoutes(mux, triggerCh, discardLogger())

	rr := httptest.NewRecorder()
	mux.ServeHTTP(rr, httptest.NewRequest("POST", "/v1/admin/archiver/run", nil))
	if rr.Code != http.StatusNotFound {
		t.Errorf("with no secret: got %d, want 404 (route unregistered)", rr.Code)
	}
}

// TestWireAdminRoutes_WithSecret_Gated verifies that with the
// secret set, the admin route is 401 without a token.
func TestWireAdminRoutes_WithSecret_Gated(t *testing.T) {
	t.Setenv("BA_AUTHD_JWT_SECRET", testSecret)
	t.Setenv("BA_AUTHD_ISSUER", "broad-announce")
	mux := http.NewServeMux()
	triggerCh := make(chan struct{}, 1)
	wireAdminRoutes(mux, triggerCh, discardLogger())

	rr := httptest.NewRecorder()
	mux.ServeHTTP(rr, httptest.NewRequest("POST", "/v1/admin/archiver/run", nil))
	if rr.Code != http.StatusUnauthorized {
		t.Errorf("with secret+no-token: status = %d, want 401", rr.Code)
	}
}

// TestAdminRunNow_Auth_Fires verifies that an authenticated user
// firing /v1/admin/archiver/run actually delivers a value on the
// trigger channel. The handler returns 200 with triggered=true.
func TestAdminRunNow_Auth_Fires(t *testing.T) {
	t.Setenv("BA_AUTHD_JWT_SECRET", testSecret)
	tok := mintTestJWT(t, testSecret, "viewer")
	mux := http.NewServeMux()
	triggerCh := make(chan struct{}, 1)
	wireAdminRoutes(mux, triggerCh, discardLogger())

	req := httptest.NewRequest("POST", "/v1/admin/archiver/run", nil)
	req.Header.Set("Authorization", "Bearer "+tok)
	rr := httptest.NewRecorder()
	mux.ServeHTTP(rr, req)
	if rr.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body = %s", rr.Code, rr.Body.String())
	}
	var body map[string]any
	if err := json.NewDecoder(rr.Body).Decode(&body); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if trig, _ := body["triggered"].(bool); !trig {
		t.Errorf("triggered = %v, want true", body["triggered"])
	}
	// Channel should have exactly one pending signal.
	select {
	case <-triggerCh:
		// good
	default:
		t.Error("triggerCh is empty after a successful fire")
	}
}

// TestAdminRunNow_Coalesces verifies the buffer-size-1 semantics:
// a second concurrent request (before the loop drains the first)
// gets 202 Accepted with triggered=false / reason=already_pending.
func TestAdminRunNow_Coalesces(t *testing.T) {
	t.Setenv("BA_AUTHD_JWT_SECRET", testSecret)
	tok := mintTestJWT(t, testSecret, "viewer")
	mux := http.NewServeMux()
	triggerCh := make(chan struct{}, 1)
	// Pre-fill the channel as if a previous run is in flight.
	triggerCh <- struct{}{}
	wireAdminRoutes(mux, triggerCh, discardLogger())

	req := httptest.NewRequest("POST", "/v1/admin/archiver/run", nil)
	req.Header.Set("Authorization", "Bearer "+tok)
	rr := httptest.NewRecorder()
	mux.ServeHTTP(rr, req)
	if rr.Code != http.StatusAccepted {
		t.Errorf("with already-pending trigger: status = %d, want 202", rr.Code)
	}
	var body map[string]any
	if err := json.NewDecoder(rr.Body).Decode(&body); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if trig, _ := body["triggered"].(bool); trig {
		t.Errorf("triggered = true, want false (coalesced)")
	}
	if reason, _ := body["reason"].(string); reason != "already_pending" {
		t.Errorf("reason = %q, want %q", reason, "already_pending")
	}
}

// TestAdminRolePolicy_ViewerCanTrigger verifies that the
// run-now endpoint is open to any authenticated user (viewer
// included). The archiver is idempotent (Postgres advisory
// lock) so a viewer firing it is safe.
func TestAdminRolePolicy_ViewerCanTrigger(t *testing.T) {
	t.Setenv("BA_AUTHD_JWT_SECRET", testSecret)
	mux := http.NewServeMux()
	triggerCh := make(chan struct{}, 1)
	wireAdminRoutes(mux, triggerCh, discardLogger())

	for _, role := range []string{"viewer", "tenant_admin", "super_admin"} {
		t.Run(role, func(t *testing.T) {
			tok := mintTestJWT(t, testSecret, role)
			// drain the channel so each subtest starts clean
			select {
			case <-triggerCh:
			default:
			}
			req := httptest.NewRequest("POST", "/v1/admin/archiver/run", nil)
			req.Header.Set("Authorization", "Bearer "+tok)
			rr := httptest.NewRecorder()
			mux.ServeHTTP(rr, req)
			if rr.Code != http.StatusOK {
				t.Errorf("%s: status = %d, want 200; body = %s", role, rr.Code, rr.Body.String())
			}
		})
	}
}
