package server

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

	agsconfig "github.com/pingcap/agent-git-service/config"
	agsserver "github.com/pingcap/agent-git-service/server"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/config"
	"github.com/tidbcloud/AgentTeam/internal/store/automation"
)

const automationTrustedClaimsHeader = "X-AgentTeam-Verified-JWT"

func TestAutomationAuthorizerDeniesClaimJWTByDefault(t *testing.T) {
	srv, err := New(config.ResourceRuntimeConfig{
		HTTPAddress: ":0",
		Auth:        config.ResourceRuntimeAuthConfig{Enabled: true, Header: "Authorization"},
		Database:    config.ResourceDatabaseConfig{DSN: meshxDatabaseDSN(t)},
		AGS:         config.AGSConfig{DatabaseDSN: prepareResolverDB(t), GitRepoDir: t.TempDir()},
	}, Options{NewAGS: func(agsconfig.Config, ...agsserver.Option) (agsServer, error) {
		return fakeAGS{handler: http.NotFoundHandler()}, nil
	}})
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, srv.Shutdown(context.Background())) })

	registerReq := httptest.NewRequest(http.MethodPost, "/api/v1/automations/runners/register", bytes.NewBufferString(`{"runnerId":"runner-forged"}`))
	registerReq.Header.Set("Authorization", automationClaimAuth("forged-user", []string{"ws-1"}, []string{"automation:runner:manage"}, false))
	registerReq.Header.Set("X-Workspace-Id", "ws-1")
	registerRec := httptest.NewRecorder()
	srv.handler.ServeHTTP(registerRec, registerReq)

	require.Equal(t, http.StatusForbidden, registerRec.Code, registerRec.Body.String())
	var body map[string]any
	require.NoError(t, json.Unmarshal(registerRec.Body.Bytes(), &body))
	require.Equal(t, "permission_denied", body["error"])
}

func TestAutomationTrustedClaimsDisableAuthorizationFallback(t *testing.T) {
	srv, err := New(config.ResourceRuntimeConfig{
		HTTPAddress: ":0",
		Auth:        config.ResourceRuntimeAuthConfig{Enabled: true, Header: "X-AgentTeam-Verified-JWT", AllowMock: true, TrustedJWTClaims: true, TrustedJWTClaimsProtectedListener: true},
		Database:    config.ResourceDatabaseConfig{DSN: meshxDatabaseDSN(t)},
		AGS:         config.AGSConfig{DatabaseDSN: prepareResolverDB(t), GitRepoDir: t.TempDir()},
	}, Options{NewAGS: func(agsconfig.Config, ...agsserver.Option) (agsServer, error) {
		return fakeAGS{handler: http.NotFoundHandler()}, nil
	}})
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, srv.Shutdown(context.Background())) })

	registerReq := httptest.NewRequest(http.MethodPost, "/api/v1/automations/runners/register", bytes.NewBufferString(`{"runnerId":"runner-forged"}`))
	registerReq.Header.Set("Authorization", automationClaimAuth("forged-user", []string{"ws-1"}, []string{"automation:runner:manage"}, false))
	registerReq.Header.Set("X-Workspace-Id", "ws-1")
	registerRec := httptest.NewRecorder()
	srv.handler.ServeHTTP(registerRec, registerReq)

	require.Equal(t, http.StatusUnauthorized, registerRec.Code, registerRec.Body.String())
	require.Contains(t, registerRec.Body.String(), "missing AgentTeam-verified JWT")
}

func TestAutomationAuthorizerDeniesCrossWorkspaceRunRead(t *testing.T) {
	srv := newAutomationAuthorizationTestServer(t)
	now := time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC)
	require.NoError(t, srv.meshxDB.Create(&automation.AutomationRun{
		ID:          "wfr_workspace_b",
		WorkspaceID: "ws-b",
		Name:        "Workspace B",
		Status:      automation.AutomationRunStatusRunning,
		CreatedAt:   now,
		UpdatedAt:   now,
	}).Error)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/automations/runs/wfr_workspace_b", nil)
	setAutomationClaimAuth(req, automationClaimAuth("user-a", []string{"ws-a"}, []string{"automation:run:read"}, false))
	req.Header.Set("X-Workspace-Id", "ws-b")
	rec := httptest.NewRecorder()
	srv.handler.ServeHTTP(rec, req)

	require.Equal(t, http.StatusForbidden, rec.Code, rec.Body.String())
	require.Contains(t, rec.Body.String(), "Automation workspace access is required")

	missingReq := httptest.NewRequest(http.MethodGet, "/api/v1/automations/runs/wfr_missing", nil)
	setAutomationClaimAuth(missingReq, automationClaimAuth("user-a", []string{"ws-a"}, []string{"automation:run:read"}, false))
	missingReq.Header.Set("X-Workspace-Id", "ws-b")
	missingRec := httptest.NewRecorder()
	srv.handler.ServeHTTP(missingRec, missingReq)
	require.Equal(t, http.StatusForbidden, missingRec.Code, missingRec.Body.String())
	require.Contains(t, missingRec.Body.String(), "Automation workspace access is required")
}

func TestAutomationAuthorizerDeniesMissingRunStartPermission(t *testing.T) {
	srv := newAutomationAuthorizationTestServer(t)
	auth := automationClaimAuth("read-user", []string{"ws-1"}, []string{"automation:repo:read", "automation:run:read"}, false)

	startReq := httptest.NewRequest(http.MethodPost, "/api/v1/automations/repositories/pingcap/agentteam/runs", bytes.NewBufferString(`{"ref":"refs/heads/main","definitionPath":".agentteam/build.yml"}`))
	setAutomationClaimAuth(startReq, auth)
	startReq.Header.Set("X-Workspace-Id", "ws-1")
	startRec := httptest.NewRecorder()
	srv.handler.ServeHTTP(startRec, startReq)
	require.Equal(t, http.StatusForbidden, startRec.Code, startRec.Body.String())
	require.Contains(t, startRec.Body.String(), "automation:run:start")

	registerReq := httptest.NewRequest(http.MethodPost, "/api/v1/automations/runners/register", bytes.NewBufferString(`{"runnerId":"runner-denied"}`))
	setAutomationClaimAuth(registerReq, auth)
	registerReq.Header.Set("X-Workspace-Id", "ws-1")
	registerRec := httptest.NewRecorder()
	srv.handler.ServeHTTP(registerRec, registerReq)
	require.Equal(t, http.StatusForbidden, registerRec.Code, registerRec.Body.String())
	require.Contains(t, registerRec.Body.String(), "automation:runner:manage")
}

func TestAutomationAuthorizerRequiresRepoWriteForSourceSave(t *testing.T) {
	var agsWrites int
	srv, err := New(config.ResourceRuntimeConfig{
		HTTPAddress: ":0",
		Auth:        config.ResourceRuntimeAuthConfig{Enabled: true, Header: automationTrustedClaimsHeader, TrustedJWTClaims: true, TrustedJWTClaimsProtectedListener: true},
		Database:    config.ResourceDatabaseConfig{DSN: meshxDatabaseDSN(t)},
		AGS:         config.AGSConfig{DatabaseDSN: prepareResolverDB(t), GitRepoDir: t.TempDir()},
	}, Options{NewAGS: func(agsconfig.Config, ...agsserver.Option) (agsServer, error) {
		return fakeAGS{handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if r.Method != http.MethodPut || r.URL.Path != "/api/v3/repos/pingcap/agentteam/contents/.agentteam/build.yml" {
				http.NotFound(w, r)
				return
			}
			agsWrites++
			w.WriteHeader(http.StatusCreated)
			_ = json.NewEncoder(w).Encode(map[string]any{
				"content": map[string]any{
					"name": "build.yml",
					"path": ".agentteam/build.yml",
					"sha":  "content-sha",
					"size": 92,
				},
				"commit": map[string]any{"sha": "commit-sha", "message": "save automation"},
			})
		})}, nil
	}})
	require.NoError(t, err)
	installLegacyAutomationSourceForTest(srv)
	t.Cleanup(func() { require.NoError(t, srv.Shutdown(context.Background())) })

	body := []byte(`{"content":"name: Build\non:\n  manual: {}\njobs:\n  build:\n    steps:\n      - run: echo hi\n","message":"save automation"}`)
	path := "/api/v1/automations/repositories/pingcap/agentteam/definitions/.agentteam%2Fbuild.yml"
	sendSave := func(subject string, permissions []string) *httptest.ResponseRecorder {
		req := httptest.NewRequest(http.MethodPut, path, bytes.NewReader(body))
		setAutomationClaimAuth(req, automationClaimAuth(subject, []string{"ws-1"}, permissions, false))
		req.Header.Set("X-Workspace-Id", "ws-1")
		rec := httptest.NewRecorder()
		srv.handler.ServeHTTP(rec, req)
		return rec
	}

	for _, tc := range []struct {
		name        string
		subject     string
		permissions []string
	}{
		{name: "repo_read", subject: "read-user", permissions: []string{"automation:repo:read"}},
		{name: "run_start", subject: "run-start-user", permissions: []string{"automation:run:start"}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			rec := sendSave(tc.subject, tc.permissions)
			require.Equal(t, http.StatusForbidden, rec.Code, rec.Body.String())
			require.Contains(t, rec.Body.String(), "automation:repo:write")
			require.Zero(t, agsWrites)
		})
	}

	writeRec := sendSave("write-user", []string{"automation:repo:write"})
	require.Equal(t, http.StatusCreated, writeRec.Code, writeRec.Body.String())
	require.Contains(t, writeRec.Body.String(), `"sha":"commit-sha"`)

	adminRec := sendSave("automation-admin", []string{"automation:*"})
	require.Equal(t, http.StatusCreated, adminRec.Code, adminRec.Body.String())
	require.Equal(t, 2, agsWrites)
}

func TestAutomationAuthorizerRequiresServiceActorForWaitResume(t *testing.T) {
	srv := newAutomationAuthorizationTestServer(t)
	now := time.Date(2026, 6, 5, 12, 30, 0, 0, time.UTC)
	require.NoError(t, srv.meshxDB.Create(&automation.AutomationRun{
		ID:          "wfr_wait",
		WorkspaceID: "ws-1",
		Name:        "Wait",
		Status:      automation.AutomationRunStatusWaiting,
		CreatedAt:   now,
		UpdatedAt:   now,
	}).Error)
	require.NoError(t, srv.meshxDB.Create(&automation.AutomationJobRun{
		ID:          "wfj_wait",
		WorkspaceID: "ws-1",
		RunID:       "wfr_wait",
		JobID:       "approval",
		JobType:     "agentteam_wait",
		Status:      automation.AutomationJobRunStatusWaiting,
		ScheduledAt: now,
		CreatedAt:   now,
		UpdatedAt:   now,
	}).Error)
	require.NoError(t, srv.meshxDB.Create(&automation.AutomationWait{
		ID:          "wfw_wait",
		WorkspaceID: "ws-1",
		RunID:       "wfr_wait",
		JobRunID:    "wfj_wait",
		WaitKind:    automation.AutomationWaitKindWaitEvent,
		Status:      automation.AutomationWaitStatusActive,
		CreatedBy:   "automation",
		CreatedAt:   now,
		UpdatedAt:   now,
	}).Error)

	humanReq := httptest.NewRequest(http.MethodPost, "/api/v1/automations/runs/wfr_wait/waits/wfw_wait/resume", bytes.NewBufferString(`{"resultJson":"{}"}`))
	setAutomationClaimAuth(humanReq, automationClaimAuth("human-user", []string{"ws-1"}, []string{"automation:wait:resume"}, false))
	humanReq.Header.Set("X-Workspace-Id", "ws-1")
	humanRec := httptest.NewRecorder()
	srv.handler.ServeHTTP(humanRec, humanReq)
	require.Equal(t, http.StatusForbidden, humanRec.Code, humanRec.Body.String())
	require.Contains(t, humanRec.Body.String(), "Automation service actor capability is required")

	adminReq := httptest.NewRequest(http.MethodPost, "/api/v1/automations/runs/wfr_wait/waits/wfw_wait/resume", bytes.NewBufferString(`{"resultJson":"{}"}`))
	setAutomationClaimAuth(adminReq, automationClaimAuth("automation-admin", []string{"ws-1"}, []string{"automation:*"}, false))
	adminReq.Header.Set("X-Workspace-Id", "ws-1")
	adminRec := httptest.NewRecorder()
	srv.handler.ServeHTTP(adminRec, adminReq)
	require.Equal(t, http.StatusForbidden, adminRec.Code, adminRec.Body.String())
	require.Contains(t, adminRec.Body.String(), "Automation service actor capability is required")

	serviceReq := httptest.NewRequest(http.MethodPost, "/api/v1/automations/runs/wfr_wait/waits/wfw_wait/resume", bytes.NewBufferString(`{"resultJson":"{}"}`))
	setAutomationClaimAuth(serviceReq, automationClaimAuth("automation-eventer", []string{"ws-1"}, []string{"automation:wait:resume", "automation:service"}, true))
	serviceReq.Header.Set("X-Workspace-Id", "ws-1")
	serviceRec := httptest.NewRecorder()
	srv.handler.ServeHTTP(serviceRec, serviceReq)
	require.Equal(t, http.StatusOK, serviceRec.Code, serviceRec.Body.String())
	require.Contains(t, serviceRec.Body.String(), `"status":"resolved"`)
}

func TestAutomationAuthorizerRequiresServiceActorForWaitSignalDelivery(t *testing.T) {
	srv := newAutomationAuthorizationTestServer(t)
	now := time.Date(2026, 6, 7, 11, 0, 0, 0, time.UTC)
	require.NoError(t, srv.meshxDB.Create(&automation.AutomationRun{
		ID:          "wfr_signal",
		WorkspaceID: "ws-1",
		Name:        "Signal",
		Status:      automation.AutomationRunStatusWaiting,
		CreatedAt:   now,
		UpdatedAt:   now,
	}).Error)
	require.NoError(t, srv.meshxDB.Create(&automation.AutomationJobRun{
		ID:          "wfj_signal",
		WorkspaceID: "ws-1",
		RunID:       "wfr_signal",
		JobID:       "wait",
		JobType:     "agentteam_wait",
		Status:      automation.AutomationJobRunStatusWaiting,
		ScheduledAt: now,
		CreatedAt:   now,
		UpdatedAt:   now,
	}).Error)
	activeKey := "ws-1:wfr_signal:wfj_signal"
	signalKey := automation.AutomationWaitSignalKey("ws-1", automation.AutomationWaitSignal{WaitKind: automation.AutomationWaitKindWaitEvent, EventType: "provider.ready", ResourceType: "provider", ResourceID: "installation-1"})
	require.NoError(t, srv.meshxDB.Create(&automation.AutomationWait{
		ID:              "wfw_signal",
		WorkspaceID:     "ws-1",
		RunID:           "wfr_signal",
		JobRunID:        "wfj_signal",
		WaitKind:        automation.AutomationWaitKindWaitEvent,
		Status:          automation.AutomationWaitStatusActive,
		ActiveKey:       &activeKey,
		SignalKey:       signalKey,
		ResourceType:    "provider",
		ResourceID:      "installation-1",
		ResumeEventType: "provider.ready",
		CreatedBy:       "automation",
		CreatedAt:       now,
		UpdatedAt:       now,
	}).Error)
	body := []byte(`{"waitKind":"wait_event","eventType":"provider.ready","resourceType":"provider","resourceId":"installation-1"}`)

	humanReq := httptest.NewRequest(http.MethodPost, "/api/v1/automations/waits:deliver-event", bytes.NewBuffer(body))
	setAutomationClaimAuth(humanReq, automationClaimAuth("human-user", []string{"ws-1"}, []string{"automation:wait:resume"}, false))
	humanReq.Header.Set("X-Workspace-Id", "ws-1")
	humanRec := httptest.NewRecorder()
	srv.handler.ServeHTTP(humanRec, humanReq)
	require.Equal(t, http.StatusForbidden, humanRec.Code, humanRec.Body.String())
	require.Contains(t, humanRec.Body.String(), "Automation service actor capability is required")

	serviceReq := httptest.NewRequest(http.MethodPost, "/api/v1/automations/waits:deliver-event", bytes.NewBuffer(body))
	setAutomationClaimAuth(serviceReq, automationClaimAuth("automation-eventer", []string{"ws-1"}, []string{"automation:wait:resume", "automation:service"}, true))
	serviceReq.Header.Set("X-Workspace-Id", "ws-1")
	serviceRec := httptest.NewRecorder()
	srv.handler.ServeHTTP(serviceRec, serviceReq)
	require.Equal(t, http.StatusOK, serviceRec.Code, serviceRec.Body.String())
	require.Contains(t, serviceRec.Body.String(), `"status":"resolved"`)
}

func TestAutomationAuthorizerRequiresEventerForPushEvent(t *testing.T) {
	srv := newAutomationAuthorizationTestServer(t)
	body := []byte(`{"deliveryKey":"github:installation:delivery-auth","ref":"refs/heads/main","after":"0000000000000000000000000000000000000000"}`)
	path := "/api/v1/automations/repositories/pingcap/agentteam/events/push"

	runStartReq := httptest.NewRequest(http.MethodPost, path, bytes.NewBuffer(body))
	setAutomationClaimAuth(runStartReq, automationClaimAuth("automation-scheduler", []string{"ws-1"}, []string{"automation:run:start"}, true))
	runStartReq.Header.Set("X-Workspace-Id", "ws-1")
	runStartRec := httptest.NewRecorder()
	srv.handler.ServeHTTP(runStartRec, runStartReq)
	require.Equal(t, http.StatusForbidden, runStartRec.Code, runStartRec.Body.String())
	require.Contains(t, runStartRec.Body.String(), "automation:eventer")

	humanEventerReq := httptest.NewRequest(http.MethodPost, path, bytes.NewBuffer(body))
	setAutomationClaimAuth(humanEventerReq, automationClaimAuth("human-user", []string{"ws-1"}, []string{"automation:eventer"}, false))
	humanEventerReq.Header.Set("X-Workspace-Id", "ws-1")
	humanEventerRec := httptest.NewRecorder()
	srv.handler.ServeHTTP(humanEventerRec, humanEventerReq)
	require.Equal(t, http.StatusForbidden, humanEventerRec.Code, humanEventerRec.Body.String())
	require.Contains(t, humanEventerRec.Body.String(), "service actor")

	eventerReq := httptest.NewRequest(http.MethodPost, path, bytes.NewBuffer(body))
	setAutomationClaimAuth(eventerReq, automationClaimAuth("automation-eventer", []string{"ws-1"}, []string{"automation:eventer"}, true))
	eventerReq.Header.Set("X-Workspace-Id", "ws-1")
	eventerRec := httptest.NewRecorder()
	srv.handler.ServeHTTP(eventerRec, eventerReq)
	require.Equal(t, http.StatusCreated, eventerRec.Code, eventerRec.Body.String())
	require.Contains(t, eventerRec.Body.String(), `"runs":[]`)
}

func newAutomationAuthorizationTestServer(t *testing.T) *Server {
	t.Helper()
	srv, err := New(config.ResourceRuntimeConfig{
		HTTPAddress: ":0",
		Auth:        config.ResourceRuntimeAuthConfig{Enabled: true, Header: automationTrustedClaimsHeader, TrustedJWTClaims: true, TrustedJWTClaimsProtectedListener: true},
		Database:    config.ResourceDatabaseConfig{DSN: meshxDatabaseDSN(t)},
		AGS:         config.AGSConfig{DatabaseDSN: prepareResolverDB(t), GitRepoDir: t.TempDir()},
	}, Options{NewAGS: func(agsconfig.Config, ...agsserver.Option) (agsServer, error) {
		return fakeAGS{handler: http.NotFoundHandler()}, nil
	}})
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, srv.Shutdown(context.Background())) })
	return srv
}

func setAutomationClaimAuth(req *http.Request, auth string) {
	req.Header.Set(automationTrustedClaimsHeader, auth)
}

func automationClaimAuth(subject string, workspaceIDs []string, permissions []string, serviceActor bool) string {
	claims := map[string]any{
		"sub":                    subject,
		"workspace_ids":          workspaceIDs,
		"automation_permissions": permissions,
	}
	if serviceActor {
		claims["actor_kind"] = "service"
	}
	return "Bearer " + jwt(claims)
}
