package admin_test

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

	"github.com/go-chi/chi/v5"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"

	"github.com/veylant/ia-gateway/internal/admin"
	"github.com/veylant/ia-gateway/internal/middleware"
	"github.com/veylant/ia-gateway/internal/routing"
)

const testTenantID = "tenant-test"

// ─── Test fixtures ────────────────────────────────────────────────────────────

func setupHandler(t *testing.T) (*admin.Handler, *routing.MemStore, *routing.RuleCache) {
	t.Helper()
	store := routing.NewMemStore()
	cache := routing.NewRuleCache(store, 30*time.Second, zap.NewNop())
	h := admin.New(store, cache, zap.NewNop())
	return h, store, cache
}

// authCtx returns a request context with tenant JWT claims.
func authCtx(tenantID string) context.Context {
	return middleware.WithClaims(context.Background(), &middleware.UserClaims{
		UserID:   "admin-user",
		TenantID: tenantID,
		Roles:    []string{"admin"},
	})
}

// newRouter builds a chi.Router with the handler routes mounted.
func newRouter(h *admin.Handler) chi.Router {
	r := chi.NewRouter()
	h.Routes(r)
	return r
}

// postJSON sends a POST with JSON body.
func postJSON(t *testing.T, router http.Handler, path string, body interface{}, ctx context.Context) *httptest.ResponseRecorder {
	t.Helper()
	b, _ := json.Marshal(body)
	req := httptest.NewRequest(http.MethodPost, path, bytes.NewReader(b))
	req = req.WithContext(ctx)
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	return rec
}

func getReq(t *testing.T, router http.Handler, path string, ctx context.Context) *httptest.ResponseRecorder {
	t.Helper()
	req := httptest.NewRequest(http.MethodGet, path, nil)
	req = req.WithContext(ctx)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	return rec
}

func deleteReq(t *testing.T, router http.Handler, path string, ctx context.Context) *httptest.ResponseRecorder {
	t.Helper()
	req := httptest.NewRequest(http.MethodDelete, path, nil)
	req = req.WithContext(ctx)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	return rec
}

// ─── Tests ────────────────────────────────────────────────────────────────────

func TestAdminHandler_Create_ReturnsCreated(t *testing.T) {
	h, _, _ := setupHandler(t)
	r := newRouter(h)

	body := map[string]interface{}{
		"name":       "finance rule",
		"priority":   10,
		"is_enabled": true,
		"conditions": []map[string]interface{}{
			{"field": "user.department", "operator": "eq", "value": "finance"},
		},
		"action": map[string]interface{}{"provider": "ollama"},
	}
	rec := postJSON(t, r, "/policies", body, authCtx(testTenantID))
	assert.Equal(t, http.StatusCreated, rec.Code)

	var got routing.RoutingRule
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&got))
	assert.Equal(t, "finance rule", got.Name)
	assert.Equal(t, testTenantID, got.TenantID)
}

func TestAdminHandler_Create_InvalidCondition_Returns400(t *testing.T) {
	h, _, _ := setupHandler(t)
	r := newRouter(h)

	body := map[string]interface{}{
		"name": "bad rule",
		"conditions": []map[string]interface{}{
			{"field": "user.unknown_field", "operator": "eq", "value": "x"},
		},
		"action": map[string]interface{}{"provider": "openai"},
	}
	rec := postJSON(t, r, "/policies", body, authCtx(testTenantID))
	assert.Equal(t, http.StatusBadRequest, rec.Code)
}

func TestAdminHandler_Create_MissingName_Returns400(t *testing.T) {
	h, _, _ := setupHandler(t)
	r := newRouter(h)

	body := map[string]interface{}{
		"conditions": []map[string]interface{}{},
		"action":     map[string]interface{}{"provider": "openai"},
	}
	rec := postJSON(t, r, "/policies", body, authCtx(testTenantID))
	assert.Equal(t, http.StatusBadRequest, rec.Code)
}

func TestAdminHandler_List_ReturnsTenantRules(t *testing.T) {
	h, store, _ := setupHandler(t)
	r := newRouter(h)

	// Seed two rules: one for testTenantID, one for another tenant.
	_, _ = store.Create(context.Background(), routing.RoutingRule{
		TenantID: testTenantID, Name: "r1", IsEnabled: true,
		Conditions: []routing.Condition{}, Action: routing.Action{Provider: "openai"},
	})
	_, _ = store.Create(context.Background(), routing.RoutingRule{
		TenantID: "other-tenant", Name: "r2", IsEnabled: true,
		Conditions: []routing.Condition{}, Action: routing.Action{Provider: "openai"},
	})

	rec := getReq(t, r, "/policies", authCtx(testTenantID))
	assert.Equal(t, http.StatusOK, rec.Code)

	var resp map[string][]routing.RoutingRule
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp))
	// Only the rule for testTenantID should be visible.
	assert.Len(t, resp["data"], 1)
	assert.Equal(t, "r1", resp["data"][0].Name)
}

func TestAdminHandler_Get_ExistingRule(t *testing.T) {
	h, store, _ := setupHandler(t)
	r := newRouter(h)

	rule, _ := store.Create(context.Background(), routing.RoutingRule{
		TenantID: testTenantID, Name: "my-rule", IsEnabled: true,
		Conditions: []routing.Condition{}, Action: routing.Action{Provider: "openai"},
	})

	rec := getReq(t, r, "/policies/"+rule.ID, authCtx(testTenantID))
	assert.Equal(t, http.StatusOK, rec.Code)

	var got routing.RoutingRule
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&got))
	assert.Equal(t, "my-rule", got.Name)
}

func TestAdminHandler_Get_NotFound_Returns404(t *testing.T) {
	h, _, _ := setupHandler(t)
	r := newRouter(h)

	rec := getReq(t, r, "/policies/nonexistent-id", authCtx(testTenantID))
	assert.Equal(t, http.StatusNotFound, rec.Code)
}

func TestAdminHandler_Delete_RemovesRule(t *testing.T) {
	h, store, _ := setupHandler(t)
	r := newRouter(h)

	rule, _ := store.Create(context.Background(), routing.RoutingRule{
		TenantID: testTenantID, Name: "to-delete", IsEnabled: true,
		Conditions: []routing.Condition{}, Action: routing.Action{Provider: "openai"},
	})

	rec := deleteReq(t, r, "/policies/"+rule.ID, authCtx(testTenantID))
	assert.Equal(t, http.StatusNoContent, rec.Code)

	// Second delete should return 404.
	rec2 := deleteReq(t, r, "/policies/"+rule.ID, authCtx(testTenantID))
	assert.Equal(t, http.StatusNotFound, rec2.Code)
}

func TestAdminHandler_TenantIsolation_CannotDeleteOtherTenantRule(t *testing.T) {
	h, store, _ := setupHandler(t)
	r := newRouter(h)

	// Rule belongs to another tenant.
	rule, _ := store.Create(context.Background(), routing.RoutingRule{
		TenantID: "other-tenant", Name: "private-rule", IsEnabled: true,
		Conditions: []routing.Condition{}, Action: routing.Action{Provider: "openai"},
	})

	// testTenantID cannot delete a rule that belongs to other-tenant — returns 404.
	rec := deleteReq(t, r, "/policies/"+rule.ID, authCtx(testTenantID))
	assert.Equal(t, http.StatusNotFound, rec.Code, "cannot delete another tenant's rule")
}

func TestAdminHandler_SeedTemplate_Catchall(t *testing.T) {
	h, _, cache := setupHandler(t)
	r := newRouter(h)

	// Pre-populate cache to verify it gets invalidated.
	_, _ = cache.Get(context.Background(), testTenantID)

	rec := postJSON(t, r, "/policies/seed/catchall", nil, authCtx(testTenantID))
	assert.Equal(t, http.StatusCreated, rec.Code)

	var got routing.RoutingRule
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&got))
	assert.Equal(t, 9999, got.Priority)
	assert.Equal(t, "openai", got.Action.Provider)
}

func TestAdminHandler_SeedTemplate_UnknownTemplate_Returns400(t *testing.T) {
	h, _, _ := setupHandler(t)
	r := newRouter(h)

	rec := postJSON(t, r, "/policies/seed/unknown", nil, authCtx(testTenantID))
	assert.Equal(t, http.StatusBadRequest, rec.Code)
}

func TestAdminHandler_NoAuth_Returns401(t *testing.T) {
	h, _, _ := setupHandler(t)
	r := newRouter(h)

	req := httptest.NewRequest(http.MethodGet, "/policies", nil)
	// No claims in context.
	rec := httptest.NewRecorder()
	r.ServeHTTP(rec, req)
	assert.Equal(t, http.StatusUnauthorized, rec.Code)
}
