package agent

import (
	"context"
	"encoding/json"
	"net/http"
	"sync"
	"testing"
	"time"

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

	workflowrun "github.com/tidbcloud/AgentTeam/internal/api/workflow/run"
	workflowrunstore "github.com/tidbcloud/AgentTeam/internal/store/workflow/run"
)

type followRunService struct {
	publicRouteRunService
	mu     sync.Mutex
	status string
	events []workflowrunstore.Event
	calls  int
	called chan struct{}
	once   sync.Once
}

func (service *followRunService) GetRun(context.Context, string, string) (workflowrunstore.Run, error) {
	service.mu.Lock()
	defer service.mu.Unlock()
	return workflowrunstore.Run{Status: service.status}, nil
}

func (service *followRunService) GetRunForAgent(ctx context.Context, workspaceID, runID string, _ workflowrun.AgentPrincipal) (workflowrunstore.Run, error) {
	return service.GetRun(ctx, workspaceID, runID)
}

func (service *followRunService) ListEventsForAgent(ctx context.Context, input workflowrun.ListEventsInput, _ workflowrun.AgentPrincipal) (workflowrun.EventPage, error) {
	return service.ListEvents(ctx, input)
}

func (service *followRunService) ListRunsForAgent(ctx context.Context, input workflowrun.ListRunsInput, _ workflowrun.AgentPrincipal) (workflowrun.RunPage, error) {
	return service.ListRuns(ctx, input)
}

func (service *followRunService) ListActivationsForAgent(ctx context.Context, input workflowrun.ListChildrenInput, _ workflowrun.AgentPrincipal) (workflowrun.ActivationPage, error) {
	return service.ListActivations(ctx, input)
}

func (service *followRunService) ListEffectsForAgent(ctx context.Context, input workflowrun.ListChildrenInput, _ workflowrun.AgentPrincipal) (workflowrun.EffectPage, error) {
	return service.ListEffects(ctx, input)
}

func (service *followRunService) GetEventForAgent(ctx context.Context, workspaceID, runID string, sequence int64, _ workflowrun.AgentPrincipal) (workflowrunstore.Event, error) {
	return service.GetEvent(ctx, workspaceID, runID, sequence)
}

func (service *followRunService) CancelForAgent(ctx context.Context, input workflowrun.CancelInput, _ workflowrun.AgentPrincipal) (workflowrun.CancelResult, error) {
	return service.Cancel(ctx, input)
}

func (service *followRunService) ListEvents(context.Context, workflowrun.ListEventsInput) (workflowrun.EventPage, error) {
	service.mu.Lock()
	defer service.mu.Unlock()
	service.calls++
	service.once.Do(func() {
		if service.called != nil {
			close(service.called)
		}
	})
	return workflowrun.EventPage{Items: append([]workflowrunstore.Event(nil), service.events...), NextCursor: "next"}, nil
}

func TestWorkflowRunLogsFollowWaitsForNewEvent(t *testing.T) {
	oldInterval, oldWait := agentWorkflowRunFollowInterval, agentWorkflowRunFollowWait
	agentWorkflowRunFollowInterval, agentWorkflowRunFollowWait = time.Millisecond, time.Second
	t.Cleanup(func() {
		agentWorkflowRunFollowInterval, agentWorkflowRunFollowWait = oldInterval, oldWait
	})

	service := &followRunService{status: workflowrunstore.RunStatusRunning, called: make(chan struct{})}
	handler := &agentWorkflowRunHandler{service: service}
	done := make(chan workflowrun.EventPage, 1)
	go func() {
		page, _, err := handler.runLogs(context.Background(), "wsp_1", "wri_1", 20, "cursor", true)
		require.NoError(t, err)
		done <- page
	}()

	select {
	case <-done:
		t.Fatal("follow returned before a new event or terminal status")
	case <-time.After(10 * time.Millisecond):
	}

	service.mu.Lock()
	service.events = []workflowrunstore.Event{{ID: "wre_2", WorkspaceID: "wsp_1", RunID: "wri_1", Seq: 2, Type: "step_completed"}}
	service.mu.Unlock()

	select {
	case page := <-done:
		require.Len(t, page.Items, 1)
	case <-time.After(time.Second):
		t.Fatal("follow did not return after a new event")
	}
}

func TestWorkflowRunLogsFollowReturnsWhenRunBecomesTerminal(t *testing.T) {
	oldInterval, oldWait := agentWorkflowRunFollowInterval, agentWorkflowRunFollowWait
	agentWorkflowRunFollowInterval, agentWorkflowRunFollowWait = time.Millisecond, time.Second
	t.Cleanup(func() {
		agentWorkflowRunFollowInterval, agentWorkflowRunFollowWait = oldInterval, oldWait
	})

	service := &followRunService{status: workflowrunstore.RunStatusRunning, called: make(chan struct{})}
	handler := &agentWorkflowRunHandler{service: service}
	done := make(chan string, 1)
	go func() {
		_, status, err := handler.runLogs(context.Background(), "wsp_1", "wri_1", 20, "cursor", true)
		require.NoError(t, err)
		done <- status
	}()

	select {
	case <-service.called:
	case <-time.After(time.Second):
		t.Fatal("follow did not perform its initial read")
	}
	service.mu.Lock()
	service.status = workflowrunstore.RunStatusSucceeded
	service.mu.Unlock()

	select {
	case status := <-done:
		require.Equal(t, workflowrunstore.RunStatusSucceeded, status)
	case <-time.After(time.Second):
		t.Fatal("follow did not return after the Run became terminal")
	}
}

// TestWorkflowRunLogsDTOPreservesDetails verifies the Agent run logs DTO
// (agentWorkflowRunLogDTO) carries the canonical RunLogResponse.Details so the
// desktop apiclient/CLI can surface it in logs/follow.
func TestWorkflowRunLogsDTOPreservesDetails(t *testing.T) {
	now := time.Now().UTC()
	event := workflowrunstore.Event{
		ID: "wre_1", WorkspaceID: "wsp_1", RunID: "wri_1", Seq: 1,
		Type: "effect_failed",
		PayloadJSON: json.RawMessage(`{
			"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62","activationId":"a00000000000000000001","nodeId":"wnd_1","kind":"send_message",
			"code":"message.delivery_failed","stage":"dispatch","path":"wnd_1",
			"problem":"The message could not be delivered","hint":"The message could not be delivered",
			"retryable":true
		}`),
		CreatedAt: now,
	}
	log := workflowrun.RunLogResponse(event)
	entry := agentWorkflowRunLogDTO{
		Sequence: log.Sequence, Time: log.Time, Level: log.Level,
		Kind: log.Kind, StepID: log.StepID, Message: log.Message,
		Details: log.Details,
	}
	require.Equal(t, "effect_failed", entry.Kind)
	require.Equal(t, "error", entry.Level)
	require.NotEmpty(t, entry.Details, "Agent run log entry must preserve Details")
	require.Contains(t, string(entry.Details), "message.delivery_failed")
	require.Contains(t, string(entry.Details), "The message could not be delivered")
	require.Contains(t, string(entry.Details), `"retryable":true`)
	require.NotContains(t, string(entry.Details), "leaseToken")
	// The DTO JSON carries details to the wire/apiclient.
	encoded, err := json.Marshal(entry)
	require.NoError(t, err)
	require.Contains(t, string(encoded), `"details":`)
	require.Contains(t, string(encoded), "message.delivery_failed")
}

// TestWorkflowRunLogsRoutePreservesEventEnvelopeContext drives the real Agent
// run logs route with a mixed event payload (envelope context + failure
// contract + operational key) and proves the wire response keeps the reviewed
// bounded activation/node/effect/kind/retry/receipt/recovery context while
// projecting only the failure contract and scrubbing operational keys.
func TestWorkflowRunLogsRoutePreservesEventEnvelopeContext(t *testing.T) {
	gin.SetMode(gin.TestMode)
	now := time.Now().UTC()
	service := &followRunService{status: workflowrunstore.RunStatusRunning}
	service.events = []workflowrunstore.Event{{
		ID: "wre_1", WorkspaceID: "wsp_1", RunID: "wri_1", Seq: 1,
		Type: "effect_retry_scheduled",
		PayloadJSON: json.RawMessage(`{
			"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62","activationId":"a00000000000000000001","nodeId":"wnd_1","kind":"send_message",
			"code":"message.delivery_failed","stage":"dispatch","path":"wnd_1",
			"problem":"The message could not be delivered","hint":"The message could not be delivered",
			"retryable":true,"retryAt":"2026-08-16T12:00:00Z",
			"messageId":"msg_1","receiptRecovery":true,
			"leaseToken":"lease_secret"
		}`),
		CreatedAt: now,
	}}
	router := workflowPublicTestRouter(t, service, nil)
	response := postWorkflowPublicCommand(t, router, "/workflow/run", map[string]any{
		"action": "logs", "runId": "wri_1",
	})
	require.Equal(t, http.StatusOK, response.Code, response.Body.String())
	text := response.Body.String()
	// Reviewed envelope context survives to the wire.
	require.Contains(t, text, "effect_retry_scheduled")
	require.Contains(t, text, `"effectId":"wfx_baa86a9e8b16589a778ee2cfce0bef62"`)
	require.Contains(t, text, `"activationId":"a00000000000000000001"`)
	require.Contains(t, text, `"nodeId":"wnd_1"`)
	require.Contains(t, text, `"retryAt":"2026-08-16T12:00:00Z"`)
	require.Contains(t, text, `"receiptRecovery":true`)
	require.Contains(t, text, `"messageId":"msg_1"`)
	// The complete persisted payload survives to the wire during testing.
	require.Contains(t, text, `"code":"message.delivery_failed"`)
	require.Contains(t, text, `"stage":"dispatch"`)
	require.Contains(t, text, "The message could not be delivered")
	require.Contains(t, text, `"leaseToken":"lease_secret"`)
}

// TestAgentRunResponseProjectsSafeErrorCode proves the trusted-Agent Run
// transport projects a storage-backed Run's ErrorCode through the shared safe
// failure-code projection: a valid stable code survives to the wire, while a
// malformed token-bearing or punctuation code never leaks (empty, no secret
// text), and the structured safe Error JSON remains actionable.
func TestAgentRunResponseProjectsSafeErrorCode(t *testing.T) {
	now := time.Now().UTC()
	run := workflowrunstore.Run{
		ID: "wri_1", InvocationID: "wiv_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Status: workflowrunstore.RunStatusFailed, ErrorCode: "message.delivery_failed",
		InputJSON: json.RawMessage(`{}`), StateJSON: json.RawMessage(`{}`),
		ErrorJSON: json.RawMessage(`{"code":"message.delivery_failed","problem":"The message could not be delivered"}`),
		QueuedAt:  now, CreatedAt: now, UpdatedAt: now,
	}
	// Valid stable code survives.
	resp := agentWorkflowRunResponse(workflowrun.RunDetailResponse(run))
	require.Equal(t, "message.delivery_failed", resp["errorCode"])
	require.Contains(t, string(resp["error"].(json.RawMessage)), "The message could not be delivered")

	// Malformed token-bearing code never leaks to the wire.
	run.ErrorCode = "token=SECRET password=hunter2"
	resp = agentWorkflowRunResponse(workflowrun.RunDetailResponse(run))
	_, hasErrorCode := resp["errorCode"]
	require.False(t, hasErrorCode, "a malformed token-bearing error code must not reach the Agent wire")
	wire, err := json.Marshal(resp)
	require.NoError(t, err)
	require.NotContains(t, string(wire), "SECRET")
	require.NotContains(t, string(wire), "hunter2")
	// The structured safe Error JSON remains actionable.
	require.Contains(t, string(resp["error"].(json.RawMessage)), "The message could not be delivered")
}

// errorCodeRunService is a storage-backed run service returning a Run and
// activation with a caller-controlled ErrorCode, so the Agent show HTTP route
// exercises the real DTO projection from a storage-shaped source.
type errorCodeRunService struct {
	publicRouteRunService
	run        workflowrunstore.Run
	activation workflowrunstore.Activation
}

func (service *errorCodeRunService) GetRun(context.Context, string, string) (workflowrunstore.Run, error) {
	return service.run, nil
}

func (service *errorCodeRunService) GetRunForAgent(ctx context.Context, workspaceID, runID string, _ workflowrun.AgentPrincipal) (workflowrunstore.Run, error) {
	return service.GetRun(ctx, workspaceID, runID)
}

func (service *errorCodeRunService) ListActivations(context.Context, workflowrun.ListChildrenInput) (workflowrun.ActivationPage, error) {
	return workflowrun.ActivationPage{Items: []workflowrunstore.Activation{service.activation}}, nil
}

func (service *errorCodeRunService) ListEffects(context.Context, workflowrun.ListChildrenInput) (workflowrun.EffectPage, error) {
	return workflowrun.EffectPage{}, nil
}

// TestAgentRunShowHTTPRouteProjectsSafeErrorCode drives the trusted-Agent show
// HTTP route with a storage-backed Run and activation carrying a malformed
// token-bearing ErrorCode and proves the malformed code never reaches the wire
// while the structured safe Error JSON remains actionable.
func TestAgentRunShowHTTPRouteProjectsSafeErrorCode(t *testing.T) {
	gin.SetMode(gin.TestMode)
	now := time.Now().UTC()
	run := workflowrunstore.Run{
		ID: "wri_1", InvocationID: "wiv_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Status: workflowrunstore.RunStatusFailed, ErrorCode: "token=SECRET password=hunter2",
		InputJSON: json.RawMessage(`{}`), StateJSON: json.RawMessage(`{}`),
		ErrorJSON: json.RawMessage(`{"code":"message.delivery_failed","problem":"The message could not be delivered"}`),
		QueuedAt:  now, CreatedAt: now, UpdatedAt: now,
	}
	activation := workflowrunstore.Activation{
		ID: "wra_6b86b273ff34fce19d6b804eff5a3f57", RunID: run.ID, NodeID: "notify", Opcode: "send_message",
		Status: workflowrunstore.ActivationStatusFailed, ErrorCode: "bad code! punctuation",
		ErrorJSON:        json.RawMessage(`{"code":"message.delivery_failed","problem":"The message could not be delivered"}`),
		ScopeContextJSON: json.RawMessage(`{}`), InputJSON: json.RawMessage(`{}`),
		CreatedAt: now, UpdatedAt: now,
	}
	service := &errorCodeRunService{run: run, activation: activation}
	router := workflowPublicTestRouter(t, service, nil)
	response := postWorkflowPublicCommand(t, router, "/workflow/run", map[string]any{
		"action": "show", "runId": "wri_1",
	})
	require.Equal(t, http.StatusOK, response.Code, response.Body.String())
	text := response.Body.String()
	// Malformed codes never leak to the wire.
	require.NotContains(t, text, "SECRET")
	require.NotContains(t, text, "hunter2")
	require.NotContains(t, text, "punctuation")
	require.NotContains(t, text, "token=SECRET")
	// The structured safe Error JSON remains actionable.
	require.Contains(t, text, "The message could not be delivered")
	require.Contains(t, text, "message.delivery_failed")
}

// TestWorkflowRunLogsRouteExposesUnreviewedEvents drives the real Agent run logs
// route and proves unreviewed event payloads are no longer redacted.
func TestWorkflowRunLogsRouteExposesUnreviewedEvents(t *testing.T) {
	gin.SetMode(gin.TestMode)
	now := time.Now().UTC()
	for _, kind := range []string{
		" run_failed ",
		"run_failed\xff",
		"run_failed=SECRET",
		"future_unknown_kind",
	} {
		service := &followRunService{status: workflowrunstore.RunStatusRunning}
		service.events = []workflowrunstore.Event{{
			ID: "wre_1", WorkspaceID: "wsp_1", RunID: "wri_1", Seq: 1,
			Type: kind,
			PayloadJSON: json.RawMessage(`{
				"activationId":"a00000000000000000001","nodeId":"wnd_1","kind":"send_message",
				"code":"message.delivery_failed","secret":"hunter2"
			}`),
			CreatedAt: now,
		}}
		router := workflowPublicTestRouter(t, service, nil)
		response := postWorkflowPublicCommand(t, router, "/workflow/run", map[string]any{
			"action": "logs", "runId": "wri_1",
		})
		require.Equal(t, http.StatusOK, response.Code, response.Body.String())
		text := response.Body.String()
		require.Contains(t, text, "hunter2")
		require.Contains(t, text, "message.delivery_failed")
	}
}

// TestWorkflowRunLogsDTOFailClosesOnScalarDetails proves the Agent run logs DTO
// carries a nil (not scalar/array/null) Details when the payload is a
// non-object, so the desktop apiclient/CLI can never surface a raw scalar.
func TestWorkflowRunLogsDTOFailClosesOnScalarDetails(t *testing.T) {
	now := time.Now().UTC()
	for _, payload := range []string{
		`"just a string"`,
		`[1,2,3]`,
		`null`,
		`42`,
	} {
		event := workflowrunstore.Event{
			ID: "wre_1", WorkspaceID: "wsp_1", RunID: "wri_1", Seq: 1,
			Type: "effect_failed", PayloadJSON: json.RawMessage(payload), CreatedAt: now,
		}
		log := workflowrun.RunLogResponse(event)
		require.Equal(t, "effect_failed", log.Kind)
		require.Nil(t, log.Details, "scalar/array/null event payload must fail closed to nil Details: %s", payload)
	}
}
