package trigger_test

import (
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	. "github.com/tidbcloud/AgentTeam/internal/api/testkit"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/provider"
	"github.com/tidbcloud/AgentTeam/internal/trigger"
)

func TestTriggerManagementEndpointsForOwner(t *testing.T) {
	t.Parallel()

	runtime := StartRuntime(t)
	fixedNow := time.Date(2026, 4, 18, 10, 0, 0, 0, time.UTC)
	runtime.SetTriggerService(trigger.New(
		slog.New(slog.NewTextHandler(io.Discard, nil)),
		runtime.Pool,
		runtime.Store.App(),
		trigger.ServiceOptions{Clock: func() time.Time { return fixedNow }},
	))

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()

	var owner dto.AuthTokensResponse
	response := DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "trigger-owner@example.com",
		"password": TestUserPassword,
		"name":     "trigger-owner",
	}, nil, &owner)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var workspaces []dto.WorkspaceResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(owner.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID
	channelID := WorkspaceAllChannelID(t, runtime, workspaceID)

	validatePayload := map[string]any{
		"name": "Deploy reminder",
		"source": map[string]any{
			"sourceKind":      "time",
			"timeMode":        "one_shot_relative",
			"relativeSeconds": 3600,
		},
		"target": map[string]any{
			"ref": channelID,
		},
		"messageTemplate": "Check deployment status.",
	}

	var preview trigger.ValidateResult
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/triggers:validate", validatePayload, WorkspaceHeaders(owner.AccessToken, workspaceID), &preview)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Nil(t, preview.Current)
	require.True(t, preview.Proposed.Validation.Valid, "%+v", preview.Proposed.Validation)
	require.Equal(t, trigger.TargetKindChannel, preview.Proposed.Target.Kind)
	require.NotNil(t, preview.EstimatedScheduledAt)

	createPayload := map[string]any{
		"name": "Friday follow-up",
		"source": map[string]any{
			"sourceKind":     "time",
			"timeMode":       "cron",
			"cronExpression": "15 10 * * 5",
			"timezone":       "UTC",
		},
		"target": map[string]any{
			"ref": channelID,
		},
		"messageTemplate": "Friday sync?",
	}

	var created trigger.TriggerView
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/triggers", createPayload, WorkspaceHeaders(owner.AccessToken, workspaceID), &created)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, created.ID)
	require.Equal(t, trigger.StatusActive, created.Status)
	require.Equal(t, trigger.TargetKindChannel, created.Target.Kind)
	require.NotNil(t, created.NextOccurrenceAt)

	var listed []trigger.TriggerView
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/triggers", nil, WorkspaceHeaders(owner.AccessToken, workspaceID), &listed)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, listed, 1)
	require.Equal(t, created.ID, listed[0].ID)

	var detail trigger.TriggerView
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/triggers/"+created.ID, nil, WorkspaceHeaders(owner.AccessToken, workspaceID), &detail)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.True(t, detail.Validation.Valid)
	require.Equal(t, "Friday sync?", detail.MessageTemplate)

	updatePayload := map[string]any{
		"name": "Friday follow-up",
		"source": map[string]any{
			"sourceKind":     "time",
			"timeMode":       "cron",
			"cronExpression": "30 10 * * 5",
			"timezone":       "UTC",
		},
		"target": map[string]any{
			"ref": channelID,
		},
		"messageTemplate": "Updated Friday sync?",
	}

	var updated trigger.TriggerView
	response = DoJSON(t, client, http.MethodPatch, server.URL+"/api/v1/workspaces/"+workspaceID+"/triggers/"+created.ID, updatePayload, WorkspaceHeaders(owner.AccessToken, workspaceID), &updated)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, "30 10 * * 5", updated.CronExpression)
	require.Equal(t, "Updated Friday sync?", updated.MessageTemplate)

	var runs []trigger.RunView
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/triggers/"+created.ID+"/runs?limit=5", nil, WorkspaceHeaders(owner.AccessToken, workspaceID), &runs)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Empty(t, runs)

	var paused trigger.TriggerView
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/triggers/"+created.ID+":pause", map[string]any{}, WorkspaceHeaders(owner.AccessToken, workspaceID), &paused)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, trigger.StatusPaused, paused.Status)

	var resumed trigger.TriggerView
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/triggers/"+created.ID+":resume", map[string]any{}, WorkspaceHeaders(owner.AccessToken, workspaceID), &resumed)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, trigger.StatusActive, resumed.Status)

	var deleteResponse struct {
		Deleted bool `json:"deleted"`
	}
	response = DoJSON(t, client, http.MethodDelete, server.URL+"/api/v1/workspaces/"+workspaceID+"/triggers/"+created.ID, nil, WorkspaceHeaders(owner.AccessToken, workspaceID), &deleteResponse)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.True(t, deleteResponse.Deleted)

	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/triggers", nil, WorkspaceHeaders(owner.AccessToken, workspaceID), &listed)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Empty(t, listed)
}

func TestTriggerManagementReplyThreadTargetIncludesParentContext(t *testing.T) {
	t.Parallel()

	runtime := StartRuntime(t)
	fixedNow := time.Date(2026, 4, 18, 10, 0, 0, 0, time.UTC)
	runtime.SetTriggerService(trigger.New(
		slog.New(slog.NewTextHandler(io.Discard, nil)),
		runtime.Pool,
		runtime.Store.App(),
		trigger.ServiceOptions{Clock: func() time.Time { return fixedNow }},
	))

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()

	var owner dto.AuthTokensResponse
	response := DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "thread-trigger-owner@example.com",
		"password": TestUserPassword,
		"name":     "thread-trigger-owner",
	}, nil, &owner)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var workspaces []dto.WorkspaceResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(owner.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID
	channelID := WorkspaceAllChannelID(t, runtime, workspaceID)

	var parentMessage message.AppMessage
	response = DoJSON(t, client, http.MethodPost, server.URL+WorkspaceScopeMessagesPath(workspaceID, channelID), map[string]any{
		"scopeId": channelID,
		"content": "Parent message for trigger thread",
	}, WorkspaceHeaders(owner.AccessToken, workspaceID), &parentMessage)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, parentMessage.ID)

	var threadSummary message.ReplyThreadSummary
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/channels/"+channelID+"/threads", map[string]any{
		"parentMessageId": parentMessage.ID,
	}, WorkspaceHeaders(owner.AccessToken, workspaceID), &threadSummary)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, threadSummary.ThreadChannelID)

	createPayload := map[string]any{
		"name": "Thread follow-up",
		"source": map[string]any{
			"sourceKind":      "time",
			"timeMode":        "one_shot_relative",
			"relativeSeconds": 300,
		},
		"target": map[string]any{
			"kind": "reply_thread",
			"ref":  threadSummary.ThreadChannelID,
		},
		"messageTemplate": "Follow up in thread.",
	}

	var created trigger.TriggerView
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/triggers", createPayload, WorkspaceHeaders(owner.AccessToken, workspaceID), &created)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, trigger.TargetKindReplyThread, created.Target.Kind)
	require.Equal(t, channelID, created.Target.ParentChannelID)
	require.Equal(t, parentMessage.ID, created.Target.ParentMessageID)

	_, err := runtime.Pool.Exec(runtime.Ctx, `
INSERT INTO automation_runs (
    id,
    workspace_id,
    automation_kind,
    automation_id,
    idempotency_key,
    source_occurrence_ref,
    source_kind,
    source_type,
    status,
    scheduled_for,
    occurred_at,
    attempt_count,
    attempted_at,
    completed_at,
    effect_kind,
    effect_ref,
    message_id,
    error_code,
    error_detail,
    target_kind,
    target_ref,
    delivery_scope_id,
    permission_check_scope_id,
    owner_member_id_snapshot,
    principal_member_id_snapshot,
    author_member_id_snapshot,
    automation_name_snapshot,
    notification_mode_snapshot,
    message_template_snapshot,
    rendered_content_preview,
    created_at,
    updated_at
)
SELECT
    $1,
    $2,
    'schedule',
    $3,
    $4,
    $5,
    'time',
    'one_shot_relative',
    'skipped',
    $6,
    $6,
    1,
    $6,
    $6,
    'none',
    '',
    NULL,
    'test_no_effect',
    'test fixture run without message effect',
    'reply_thread',
    $7,
    $7,
    $7,
    m.id,
    m.id,
    m.id,
    $8,
    'none',
    'Follow up in thread.',
    'Follow up in thread.',
    $6,
    $6
FROM members m
WHERE m.workspace_id = $2
  AND m.user_id = $9
LIMIT 1
`, "arun_reply_thread_context", workspaceID, created.ID, "idem_reply_thread_context", fixedNow.Format(time.RFC3339Nano), fixedNow, threadSummary.ThreadChannelID, created.Name, owner.User.ID)
	require.NoError(t, err)

	var runs []trigger.RunView
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/triggers/"+created.ID+"/runs?limit=5", nil, WorkspaceHeaders(owner.AccessToken, workspaceID), &runs)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, runs, 1)
	require.Equal(t, threadSummary.ThreadChannelID, runs[0].Target.Ref)
	require.Equal(t, channelID, runs[0].Target.ParentChannelID)
	require.Equal(t, parentMessage.ID, runs[0].Target.ParentMessageID)
}

func TestTriggerManagementRejectsGitHubGiteaProviderWatchCreation(t *testing.T) {
	t.Parallel()

	runtime := StartRuntime(t)
	fixedNow := time.Date(2026, 4, 18, 10, 0, 0, 0, time.UTC)
	runtime.SetTriggerService(trigger.New(
		slog.New(slog.NewTextHandler(io.Discard, nil)),
		runtime.Pool,
		runtime.Store.App(),
		trigger.ServiceOptions{Clock: func() time.Time { return fixedNow }},
	))

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()

	var owner dto.AuthTokensResponse
	response := DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "trigger-event-owner@example.com",
		"password": TestUserPassword,
		"name":     "trigger-event-owner",
	}, nil, &owner)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var workspaces []dto.WorkspaceResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(owner.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID
	channelID := WorkspaceAllChannelID(t, runtime, workspaceID)

	installation, err := setupGitHubAppForWorkspaceAdminForTest(runtime.Ctx, runtime.Store.App(), owner.User.ID, workspaceID, provider.UpsertGitHubAppConfigParams{
		AppID:         4242,
		AppSlug:       "agentteam-http",
		AppName:       "AgentTeam HTTP",
		HTMLURL:       "https://github.com/apps/agentteam-http",
		ClientID:      "Iv1.http",
		ClientSecret:  "client-secret",
		PrivateKeyPEM: "-----BEGIN RSA PRIVATE KEY-----\nkey\n-----END RSA PRIVATE KEY-----",
		WebhookSecret: "webhook-secret",
		OwnerLogin:    "acme",
		OwnerID:       7,
	})
	require.NoError(t, err)

	connection, err := runtime.Store.App().UpsertAppConnection(runtime.Ctx, app.UpsertAppConnectionParams{
		InstallationID:     installation.ID,
		ExternalSubjectRef: "789",
		DisplayLabel:       "acme",
		Credentials:        map[string]any{"webhook_secret": "webhook-secret"},
		CreatedByUserID:    owner.User.ID,
	})
	require.NoError(t, err)

	badTemplatePayload := map[string]any{
		"name": "GitHub watch",
		"source": map[string]any{
			"sourceKind": "event",
			"eventSubscription": map[string]any{
				"eventKinds": []string{"github_pull_request.*"},
				"filter": map[string]any{
					"resourceIdentifier": "github:acme/agentteam#42",
					"connectionRef":      connection.ExternalSubjectRef,
				},
			},
		},
		"target": map[string]any{
			"ref": channelID,
		},
		"messageTemplate": "Manual header\n{{event_summary}}\n{{resource_url}}{{external_body_blockquote}}",
	}

	var apiError map[string]any
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/triggers:validate", badTemplatePayload, WorkspaceHeaders(owner.AccessToken, workspaceID), &apiError)
	require.Equal(t, http.StatusBadRequest, response.StatusCode)
	require.Equal(t, "GitHub/Gitea provider watches must be managed through the app page or authorized provider app MCP", apiError["error"])

	apiError = map[string]any{}
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/triggers", badTemplatePayload, WorkspaceHeaders(owner.AccessToken, workspaceID), &apiError)
	require.Equal(t, http.StatusBadRequest, response.StatusCode)
	require.Equal(t, "GitHub/Gitea provider watches must be managed through the app page or authorized provider app MCP", apiError["error"])
}

func TestTriggerManagementRejectsProviderMismatchedEventKinds(t *testing.T) {
	t.Parallel()

	runtime := StartRuntime(t)
	fixedNow := time.Date(2026, 4, 18, 10, 0, 0, 0, time.UTC)
	runtime.SetTriggerService(trigger.New(
		slog.New(slog.NewTextHandler(io.Discard, nil)),
		runtime.Pool,
		runtime.Store.App(),
		trigger.ServiceOptions{Clock: func() time.Time { return fixedNow }},
	))

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()

	var owner dto.AuthTokensResponse
	response := DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "trigger-event-kind-mismatch@example.com",
		"password": TestUserPassword,
		"name":     "trigger-event-kind-mismatch",
	}, nil, &owner)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var workspaces []dto.WorkspaceResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(owner.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID
	channelID := WorkspaceAllChannelID(t, runtime, workspaceID)

	installation, err := setupGitHubAppForWorkspaceAdminForTest(runtime.Ctx, runtime.Store.App(), owner.User.ID, workspaceID, provider.UpsertGitHubAppConfigParams{
		AppID:         4242,
		AppSlug:       "agentteam-http",
		AppName:       "AgentTeam HTTP",
		HTMLURL:       "https://github.com/apps/agentteam-http",
		ClientID:      "Iv1.http",
		ClientSecret:  "client-secret",
		PrivateKeyPEM: "-----BEGIN RSA PRIVATE KEY-----\nkey\n-----END RSA PRIVATE KEY-----",
		WebhookSecret: "webhook-secret",
		OwnerLogin:    "acme",
		OwnerID:       7,
	})
	require.NoError(t, err)

	connection, err := runtime.Store.App().UpsertAppConnection(runtime.Ctx, app.UpsertAppConnectionParams{
		InstallationID:     installation.ID,
		ExternalSubjectRef: "789",
		DisplayLabel:       "acme",
		Credentials:        map[string]any{"webhook_secret": "webhook-secret"},
		CreatedByUserID:    owner.User.ID,
	})
	require.NoError(t, err)

	mismatchedPayload := map[string]any{
		"name": "GitHub watch",
		"source": map[string]any{
			"sourceKind": "event",
			"eventSubscription": map[string]any{
				"eventKinds": []string{"gitea_pull_request.*"},
				"filter": map[string]any{
					"resourceIdentifier": "github:acme/agentteam#42",
					"connectionRef":      connection.ExternalSubjectRef,
				},
			},
		},
		"target": map[string]any{
			"ref": channelID,
		},
		"messageTemplate": "{{event_summary}}\n{{resource_url}}{{external_body_blockquote}}",
	}

	var apiError map[string]any
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/triggers:validate", mismatchedPayload, WorkspaceHeaders(owner.AccessToken, workspaceID), &apiError)
	require.Equal(t, http.StatusBadRequest, response.StatusCode)
	require.Equal(t, "GitHub/Gitea provider watches must be managed through the app page or authorized provider app MCP", apiError["error"])

	apiError = map[string]any{}
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/triggers", mismatchedPayload, WorkspaceHeaders(owner.AccessToken, workspaceID), &apiError)
	require.Equal(t, http.StatusBadRequest, response.StatusCode)
	require.Equal(t, "GitHub/Gitea provider watches must be managed through the app page or authorized provider app MCP", apiError["error"])
}
