package httpapi

import (
	"context"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/config"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)

func TestAdminTasksCrossTenantQueryAndRedaction(t *testing.T) {
	databaseURL := strings.TrimSpace(os.Getenv("AI_GATEWAY_TEST_DATABASE_URL"))
	if databaseURL == "" {
		t.Skip("set AI_GATEWAY_TEST_DATABASE_URL to run the PostgreSQL integration flow")
	}

	ctx := context.Background()
	applyMigration(t, ctx, databaseURL)
	db, err := store.Connect(ctx, databaseURL)
	if err != nil {
		t.Fatalf("connect store: %v", err)
	}
	defer db.Close()

	suffix := strconv.FormatInt(time.Now().UnixNano(), 10)
	createTenant := func(key, name string) store.GatewayTenant {
		t.Helper()
		tenant, err := db.CreateTenant(ctx, store.GatewayTenantInput{
			TenantKey: key + "-" + suffix,
			Name:      name,
			Source:    "gateway",
			Status:    "active",
		})
		if err != nil {
			t.Fatalf("create tenant %s: %v", name, err)
		}
		return tenant
	}
	createUser := func(tenant store.GatewayTenant, prefix string, roles []string) store.GatewayUser {
		t.Helper()
		username := prefix + "_" + suffix
		user, err := db.CreateGatewayUser(ctx, store.GatewayUserInput{
			UserKey:         "gateway:" + username,
			Username:        username,
			DisplayName:     prefix + " display",
			Email:           username + "@example.com",
			Source:          "gateway",
			GatewayTenantID: tenant.ID,
			TenantKey:       tenant.TenantKey,
			Roles:           roles,
			Status:          "active",
		})
		if err != nil {
			t.Fatalf("create gateway user %s: %v", prefix, err)
		}
		return user
	}
	authUser := func(user store.GatewayUser, apiKeyName string) *auth.User {
		return &auth.User{
			ID:              user.ID,
			Username:        user.Username,
			Roles:           user.Roles,
			Source:          user.Source,
			GatewayUserID:   user.ID,
			GatewayTenantID: user.GatewayTenantID,
			TenantKey:       user.TenantKey,
			APIKeyID:        "key-" + apiKeyName + "-" + suffix,
			APIKeyName:      apiKeyName,
			APIKeyPrefix:    "sk-" + apiKeyName,
		}
	}
	createTask := func(user *auth.User, model, secret string) store.GatewayTask {
		t.Helper()
		task, err := db.CreateTask(ctx, store.CreateTaskInput{
			Kind:    "chat.completions",
			Model:   model,
			RunMode: "simulation",
			Request: map[string]any{
				"model": model,
				"diagnostic": map[string]any{
					"apiKey": secret,
				},
			},
		}, user)
		if err != nil {
			t.Fatalf("create task for %s: %v", user.Username, err)
		}
		return task
	}

	tenantA := createTenant("admin-task-a", "Admin Task Tenant A")
	tenantB := createTenant("admin-task-b", "Admin Task Tenant B")
	operator := createUser(tenantA, "admin_task_operator", []string{"operator"})
	userA := createUser(tenantA, "admin_task_user_a", []string{"user"})
	userB := createUser(tenantB, "admin_task_user_b", []string{"user"})
	userAAuth := authUser(userA, "alpha-key")
	userBAuth := authUser(userB, "bravo-key")

	modelA := "admin-task-model-a-" + suffix
	modelB := "admin-task-model-b-" + suffix
	taskA := createTask(userAAuth, modelA, "alpha-secret-"+suffix)
	taskBSecret := "bravo-secret-" + suffix
	taskB := createTask(userBAuth, modelB, taskBSecret)

	platform, err := db.CreatePlatform(ctx, store.CreatePlatformInput{
		Provider:    "openai",
		PlatformKey: "admin-task-platform-" + suffix,
		Name:        "Admin Task Platform " + suffix,
		BaseURL:     "https://example.invalid/v1",
		AuthType:    "bearer",
		Credentials: map[string]any{"apiKey": "platform-secret-" + suffix},
		Status:      "enabled",
	})
	if err != nil {
		t.Fatalf("create platform: %v", err)
	}
	attemptID, err := db.CreateTaskAttempt(ctx, store.CreateTaskAttemptInput{
		TaskID:     taskB.ID,
		AttemptNo:  1,
		PlatformID: platform.ID,
		QueueKey:   "admin-task-integration-" + suffix,
		Status:     "succeeded",
		Simulated:  true,
	})
	if err != nil {
		t.Fatalf("create task attempt: %v", err)
	}
	if _, err := db.Pool().Exec(ctx, `
UPDATE gateway_tasks
SET status = 'succeeded',
    model_type = 'text_generate',
    requested_model = $2,
    resolved_model = $2,
    request_id = $3,
    billing_status = 'settled',
    result = $4::jsonb,
    finished_at = now(),
    updated_at = now()
WHERE id = $1::uuid`,
		taskB.ID,
		modelB,
		"request-admin-task-"+suffix,
		`{"authorization":"Bearer result-secret","output":"ok"}`,
	); err != nil {
		t.Fatalf("enrich task: %v", err)
	}
	if _, err := db.Pool().Exec(ctx, `
UPDATE gateway_task_attempts
SET request_snapshot = $2::jsonb,
    response_snapshot = $3::jsonb,
    finished_at = now()
WHERE id = $1::uuid`,
		attemptID,
		`{"headers":{"authorization":"Bearer attempt-secret"}}`,
		`{"cookie":"attempt-cookie","status":"ok"}`,
	); err != nil {
		t.Fatalf("enrich task attempt: %v", err)
	}
	if _, err := db.Pool().Exec(ctx, `
INSERT INTO gateway_task_param_preprocessing_logs (
  task_id, attempt_id, attempt_no, model_type, platform_id, client_id,
  changed, change_count, actual_input, converted_output, changes, model_snapshot
)
VALUES (
  $1::uuid, $2::uuid, 1, 'text_generate', $3::uuid, $4,
  true, 1, $5::jsonb, $6::jsonb, $7::jsonb, '{}'::jsonb
)`,
		taskB.ID,
		attemptID,
		platform.ID,
		"admin-task-client-"+suffix,
		`{"password":"input-secret","prompt":"hello"}`,
		`{"token":"converted-secret","prompt":"hello"}`,
		`[{"path":"credentials.secret","before":"old-secret","after":"new-secret"}]`,
	); err != nil {
		t.Fatalf("create parameter preprocessing log: %v", err)
	}

	const jwtSecret = "admin-task-integration-jwt-secret"
	handlerCtx, cancelHandler := context.WithCancel(ctx)
	defer cancelHandler()
	server := httptest.NewServer(NewServerWithContext(handlerCtx, config.Config{
		AppEnv:            "test",
		HTTPAddr:          ":0",
		DatabaseURL:       databaseURL,
		IdentityMode:      "hybrid",
		JWTSecret:         jwtSecret,
		CORSAllowedOrigin: "*",
	}, db, slog.New(slog.NewTextHandler(io.Discard, nil))))
	defer server.Close()

	authenticator := auth.New(jwtSecret, "", "")
	signToken := func(user *auth.User) string {
		t.Helper()
		token, err := authenticator.SignJWT(user, time.Hour)
		if err != nil {
			t.Fatalf("sign JWT for %s: %v", user.Username, err)
		}
		return token
	}
	operatorToken := signToken(&auth.User{
		ID:              operator.ID,
		Username:        operator.Username,
		Roles:           operator.Roles,
		Source:          operator.Source,
		GatewayUserID:   operator.ID,
		GatewayTenantID: operator.GatewayTenantID,
		TenantKey:       operator.TenantKey,
	})
	userAToken := signToken(userAAuth)

	doJSON(t, server.URL, http.MethodGet, "/api/admin/tasks", userAToken, nil, http.StatusForbidden, nil)

	query := url.Values{
		"q":             {taskB.ID},
		"tenantId":      {tenantB.ID},
		"userId":        {userB.ID},
		"status":        {"succeeded"},
		"platformId":    {platform.ID},
		"model":         {modelB},
		"modelType":     {"text_generate"},
		"runMode":       {"simulation"},
		"billingStatus": {"settled"},
		"apiKey":        {"bravo-key"},
		"page":          {"1"},
		"pageSize":      {"10"},
	}
	var listResponse struct {
		Items    []store.AdminGatewayTask `json:"items"`
		Total    int                      `json:"total"`
		Page     int                      `json:"page"`
		PageSize int                      `json:"pageSize"`
	}
	doJSON(t, server.URL, http.MethodGet, "/api/admin/tasks?"+query.Encode(), operatorToken, nil, http.StatusOK, &listResponse)
	if len(listResponse.Items) != 1 || listResponse.Total != 1 || listResponse.Page != 1 || listResponse.PageSize != 10 {
		t.Fatalf("unexpected filtered admin task list: %+v", listResponse)
	}
	listed := listResponse.Items[0]
	if listed.ID != taskB.ID || listed.AdminContext.User == nil || listed.AdminContext.User.ID != userB.ID ||
		listed.AdminContext.Tenant == nil || listed.AdminContext.Tenant.ID != tenantB.ID ||
		listed.AdminContext.LatestPlatform == nil || listed.AdminContext.LatestPlatform.ID != platform.ID {
		t.Fatalf("admin task list should expose cross-tenant identity and platform summaries: %+v", listed)
	}
	if nestedString(listed.Request, "diagnostic", "apiKey") != "***" ||
		nestedString(listed.Result, "authorization") != "***" ||
		nestedString(listed.Attempts[0].RequestSnapshot, "headers", "authorization") != "***" {
		t.Fatalf("admin task list should mask task and attempt secrets: %+v", listed)
	}

	var maskedDetail store.AdminGatewayTask
	doJSON(t, server.URL, http.MethodGet, "/api/admin/tasks/"+taskB.ID, operatorToken, nil, http.StatusOK, &maskedDetail)
	if nestedString(maskedDetail.Request, "diagnostic", "apiKey") != "***" {
		t.Fatalf("admin task detail should be masked by default: %+v", maskedDetail.Request)
	}
	var fullDetail store.AdminGatewayTask
	doJSON(t, server.URL, http.MethodGet, "/api/admin/tasks/"+taskB.ID+"?sensitive=full", operatorToken, nil, http.StatusOK, &fullDetail)
	if nestedString(fullDetail.Request, "diagnostic", "apiKey") != taskBSecret {
		t.Fatalf("explicit full task detail should expose stored content: %+v", fullDetail.Request)
	}

	var maskedLogs struct {
		Items []store.TaskParamPreprocessingLog `json:"items"`
	}
	doJSON(t, server.URL, http.MethodGet, "/api/admin/tasks/"+taskB.ID+"/param-preprocessing", operatorToken, nil, http.StatusOK, &maskedLogs)
	if len(maskedLogs.Items) != 1 ||
		nestedString(maskedLogs.Items[0].ActualInput, "password") != "***" ||
		nestedString(maskedLogs.Items[0].ConvertedOutput, "token") != "***" {
		t.Fatalf("admin task preprocessing logs should be masked by default: %+v", maskedLogs.Items)
	}
	var fullLogs struct {
		Items []store.TaskParamPreprocessingLog `json:"items"`
	}
	doJSON(t, server.URL, http.MethodGet, "/api/admin/tasks/"+taskB.ID+"/param-preprocessing?sensitive=full", operatorToken, nil, http.StatusOK, &fullLogs)
	if len(fullLogs.Items) != 1 ||
		nestedString(fullLogs.Items[0].ActualInput, "password") != "input-secret" ||
		nestedString(fullLogs.Items[0].ConvertedOutput, "token") != "converted-secret" {
		t.Fatalf("explicit full preprocessing logs should expose stored content: %+v", fullLogs.Items)
	}

	var ownTasks struct {
		Items []store.GatewayTask `json:"items"`
	}
	doJSON(t, server.URL, http.MethodGet, "/api/workspace/tasks?pageSize=100", userAToken, nil, http.StatusOK, &ownTasks)
	if !gatewayTaskListHasID(ownTasks.Items, taskA.ID) || gatewayTaskListHasID(ownTasks.Items, taskB.ID) {
		t.Fatalf("workspace task list must remain scoped to the current user: %+v", ownTasks.Items)
	}
	doJSON(t, server.URL, http.MethodGet, "/api/workspace/tasks/"+taskB.ID, userAToken, nil, http.StatusNotFound, nil)
	doJSON(t, server.URL, http.MethodGet, "/api/workspace/tasks/"+taskB.ID+"/param-preprocessing", userAToken, nil, http.StatusNotFound, nil)
}

func nestedString(value map[string]any, path ...string) string {
	var current any = value
	for _, key := range path {
		object, ok := current.(map[string]any)
		if !ok {
			return ""
		}
		current = object[key]
	}
	result, _ := current.(string)
	return result
}

func gatewayTaskListHasID(items []store.GatewayTask, taskID string) bool {
	for _, item := range items {
		if item.ID == taskID {
			return true
		}
	}
	return false
}
