package run

import (
	"encoding/json"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

func TestUnifiedProjectorRejectsSecretBearingWellTypedFields(t *testing.T) {
	now := time.Now().UTC()
	errorJSON := json.RawMessage(`{
		"code":"message.target_invalid",
		"message":"SELECT password FROM users WHERE tenant='acme'",
		"stage":"reducer",
		"path":"notify",
		"problem":"SELECT problem text password=hunter2",
		"hint":"SELECT hint text token=abc"
	}`)
	run := store.Run{
		ID: "wri_1", InvocationID: "wiv_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Status: store.RunStatusFailed, ErrorCode: "message.target_invalid", ErrorJSON: errorJSON,
		InputJSON: json.RawMessage(`{}`), StateJSON: json.RawMessage(`{}`),
		QueuedAt: now, CreatedAt: now, UpdatedAt: now,
	}
	response := RunDetailResponse(run)
	raw, err := json.Marshal(response)
	require.NoError(t, err)
	text := string(raw)
	for _, secret := range []string{"hunter2", "password", "acme", "SELECT", "token=abc"} {
		require.NotContains(t, text, secret)
	}
	require.Contains(t, text, "The message target is invalid")
	require.NotContains(t, text, `"message":`)
}

// TestUnifiedProjectorIntegerRoundTrip proves json.Number and concrete integer
// forms (int/int64/float64 integral) in diagnostic index and truncation counts
// are accepted only after integrality/range checks; fractional, out-of-range, or
// string values fail closed.
func TestUnifiedProjectorIntegerRoundTrip(t *testing.T) {
	now := time.Now().UTC()
	// json.Number is the decoded form produced by UseNumber.
	errorJSON := json.RawMessage(`{
		"code":"message.target_invalid",
		"diagnostics":[
			{"code":"message.target_invalid","index":2,"message":"ok"},
			{"code":"message.target_invalid","index":3.5,"message":"fractional"},
			{"code":"message.target_invalid","index":"4","message":"string-index"}
		],
		"workItem":{"key":"child","index":1},
		"truncation":{"diagnosticsTruncated":true,"omittedCount":5,"serializedBytes":1200}
	}`)
	run := store.Run{
		ID: "wri_1", InvocationID: "wiv_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Status: store.RunStatusFailed, ErrorCode: "message.target_invalid", ErrorJSON: errorJSON,
		InputJSON: json.RawMessage(`{}`), StateJSON: json.RawMessage(`{}`),
		QueuedAt: now, CreatedAt: now, UpdatedAt: now,
	}
	response := RunDetailResponse(run)
	raw, err := json.Marshal(response)
	require.NoError(t, err)
	text := string(raw)
	// The integral diagnostic index survives; the fractional and string indices
	// fail closed (dropped from the projected diagnostic).
	require.Contains(t, text, `"index":2`)
	require.NotContains(t, text, `"index":3`)
	require.NotContains(t, text, `"index":4`)
	// The diagnostics with malformed indices still carry their safe message but
	// no index is projected for them (only index 2 and the workItem index 1).
	require.Contains(t, text, `"message":"fractional"`)
	require.Contains(t, text, `"message":"string-index"`)
	require.NotContains(t, text, `"index":3`)
	require.NotContains(t, text, `"index":4`)
	// The workItem integral index survives.
	require.Contains(t, text, `"key":"child"`)
	require.Contains(t, text, `"index":1`)
	// The truncation counts survive via the integer helper.
	require.Contains(t, text, `"omittedCount":5`)
	require.Contains(t, text, `"serializedBytes":1200`)
	require.Contains(t, text, `"diagnosticsTruncated":true`)
}

// TestUnifiedProjectorMergesPriorAndFreshTruncation proves the shared budget
// deterministically merges a prior truncation report with a fresh cut for
// count/field/serialized/minimal-fallback, so a replay/re-read preserves the
// earlier loss instead of recomputing it away.
func TestUnifiedProjectorMergesPriorAndFreshTruncation(t *testing.T) {
	now := time.Now().UTC()
	// A persisted error_json carrying a prior truncation report plus a fresh
	// oversized diagnostics array that needs a new cut.
	errorJSON := json.RawMessage(`{
		"code":"message.target_invalid",
		"diagnostics":[
			{"code":"message.target_invalid","path":"a","message":"` + strings.Repeat("x", 2048) + `"},
			{"code":"message.target_invalid","path":"b"}
		],
		"truncation":{"diagnosticsTruncated":true,"omittedCount":3,"truncatedFields":["code"],"serializedBytes":900}
	}`)
	run := store.Run{
		ID: "wri_1", InvocationID: "wiv_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Status: store.RunStatusFailed, ErrorCode: "message.target_invalid", ErrorJSON: errorJSON,
		InputJSON: json.RawMessage(`{}`), StateJSON: json.RawMessage(`{}`),
		QueuedAt: now, CreatedAt: now, UpdatedAt: now,
	}
	response := RunDetailResponse(run)
	raw, err := json.Marshal(response)
	require.NoError(t, err)
	text := string(raw)
	// The prior omittedCount is accumulated with the fresh cut (>= 3).
	require.Contains(t, text, `"omittedCount":`)
	require.Contains(t, text, `"diagnosticsTruncated":true`)
	// The bounded diagnostic survives; the oversized message is cut.
	require.Contains(t, text, `"path":"b"`)
	require.NotContains(t, text, strings.Repeat("x", 2000))
}

// TestUnifiedProjectorEffectResultFailureFields proves the EffectResult failure
// fields route through the same unified projector: the raw failureMessage is
// dropped, problem/hint derive from the reviewed code, the failureCode survives,
// and user output is preserved only at its explicit boundary.
func TestUnifiedProjectorEffectResultFailureFields(t *testing.T) {
	now := time.Now().UTC()
	resultJSON := json.RawMessage(`{
		"port":"failed",
		"failureCode":"agent.failed",
		"failureMessage":"no answer password=hunter2 tenant=acme",
		"failureProblem":"SELECT explicit password=hunter2",
		"failureHint":"SELECT explicit token=abc",
		"output":{"failure":"user output","failureCode":"user_code"}
	}`)
	effect := store.Effect{
		ID: "wfx_baa86a9e8b16589a778ee2cfce0bef62", RunID: "wri_1", ActivationID: "wra_6b86b273ff34fce19d6b804eff5a3f57", Kind: store.EffectKindMessage,
		Status: store.EffectStatusFailed, ResultJSON: resultJSON,
		CreatedAt: now, UpdatedAt: now,
	}
	response := EffectResponse(effect)
	raw, err := json.Marshal(response)
	require.NoError(t, err)
	text := string(raw)
	for _, secret := range []string{"hunter2", "password", "acme", "token=abc", "no answer"} {
		require.NotContains(t, text, secret)
	}
	require.Contains(t, text, "agent.failed")
	require.NotContains(t, text, "failureMessage")
	// The reviewed code map derives the problem/hint; an unreviewed code like
	// agent.failed fails closed to the generic bounded problem (explicit
	// persisted problem/hint values are never trusted).
	require.Contains(t, text, "Workflow Run failed")
	// User output survives exactly.
	require.Contains(t, text, "user output")
	require.Contains(t, text, "user_code")
}
