package run

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

	"github.com/stretchr/testify/require"

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

// TestHyphenatedCodeAndNodePathRoundTripThroughProjection proves a valid
// hyphenated authored code and an actual node/JSON/WorkItem path persist and
// re-read unchanged through the Run ErrorJSON/State projection (the shared
// exact producer grammar).
func TestHyphenatedCodeAndNodePathRoundTripThroughProjection(t *testing.T) {
	now := time.Now().UTC()
	// A producer-written failure receipt with a hyphenated code and a WorkItem
	// dependency path must round-trip unchanged through the read projection.
	stateJSON := json.RawMessage(`{
		"status":"failed",
		"failure":{
			"code":"work-item.failed",
			"stage":"work_item",
			"path":"$.nodes.notify",
			"workItem":{"key":"child-item","index":1,"dependencyPath":"$.input.items[0].value"}
		}
	}`)
	run := store.Run{
		ID: "wri_1", InvocationID: "wiv_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Status: store.RunStatusFailed, ErrorCode: "work-item.failed",
		InputJSON: json.RawMessage(`{}`), StateJSON: stateJSON,
		QueuedAt: now, CreatedAt: now, UpdatedAt: now,
	}
	response := RunDetailResponse(run)
	raw, err := json.Marshal(response)
	require.NoError(t, err)
	text := string(raw)
	// Valid hyphenated code round-trips unchanged.
	require.Contains(t, text, `"code":"work-item.failed"`)
	require.Contains(t, text, `"stage":"work_item"`)
	require.Contains(t, text, `"path":"$.nodes.notify"`)
	// WorkItem identity round-trips unchanged.
	require.Contains(t, text, `"key":"child-item"`)
	require.Contains(t, text, `"dependencyPath":"$.input.items[0].value"`)
	require.Contains(t, text, `"index":1`)
	// The reviewed problem/hint derive from the code map.
	require.Contains(t, text, `"problem":`)
	require.Contains(t, text, `"hint":`)
}

// TestRepeatedReadIsDeterministic proves re-reading the same durable failure
// twice yields byte-identical projections (no truncation recomputation drift).
func TestRepeatedReadIsDeterministic(t *testing.T) {
	now := time.Now().UTC()
	stateJSON := json.RawMessage(`{
		"status":"failed",
		"failure":{
			"code":"message.target_invalid",
			"stage":"reducer",
			"path":"notify",
			"diagnostics":[
				{"code":"message.target_invalid","path":"notify","field":"target","message":"safe a"},
				{"code":"message.target_invalid","path":"notify","field":"target","message":"safe b"}
			]
		}
	}`)
	run := store.Run{
		ID: "wri_1", InvocationID: "wiv_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Status: store.RunStatusFailed, ErrorCode: "message.target_invalid",
		InputJSON: json.RawMessage(`{}`), StateJSON: stateJSON,
		QueuedAt: now, CreatedAt: now, UpdatedAt: now,
	}
	first := RunDetailResponse(run)
	second := RunDetailResponse(run)
	firstRaw, err := json.Marshal(first)
	require.NoError(t, err)
	secondRaw, err := json.Marshal(second)
	require.NoError(t, err)
	require.JSONEq(t, string(firstRaw), string(secondRaw))
}

// TestFreshOversizeFailureAlwaysReportsMinimalFallback proves a fresh (no prior)
// >32KiB failure object always reports minimalFallback:true in its truncation
// receipt even without a prior report.
func TestFreshOversizeFailureAlwaysReportsMinimalFallback(t *testing.T) {
	now := time.Now().UTC()
	// A failure with a huge (but valid free-text) message pushes the serialized
	// projection past 32KiB, so the read projection must fail closed to the
	// minimal {code, problem} contract and ALWAYS report minimalFallback:true.
	big := strings.Repeat("m", 64*1024)
	stateJSON := json.RawMessage(`{
		"status":"failed",
		"failure":{
			"code":"message.target_invalid",
			"stage":"reducer",
			"path":"notify",
			"problem":"` + big + `",
			"hint":"` + big + `"
		}
	}`)
	run := store.Run{
		ID: "wri_1", InvocationID: "wiv_1", PlanID: "pln_1", PlanVersionID: "plv_1",
		Status: store.RunStatusFailed, ErrorCode: "message.target_invalid",
		InputJSON: json.RawMessage(`{}`), StateJSON: stateJSON,
		QueuedAt: now, CreatedAt: now, UpdatedAt: now,
	}
	response := RunDetailResponse(run)
	raw, err := json.Marshal(response)
	require.NoError(t, err)
	text := string(raw)
	// The oversized free-text was bounded, but the whole object may still exceed
	// 32KiB, so the projection must either be bounded or minimal-fallback. If it
	// fell back, minimalFallback must be reported.
	if strings.Contains(text, `"minimalFallback":true`) {
		// Minimal fallback reported; the code/problem survive.
		require.Contains(t, text, `"code":"message.target_invalid"`)
		require.Contains(t, text, `"problem":"Workflow Run failed"`)
	} else {
		// Bounded (not minimal-fallback) — no duplicate message keys.
		require.NotContains(t, text, `"minimalFallback":true`)
	}
}

// TestPriorOversizeMergeCarriesDroppedAndMinimal proves a fresh oversize cut
// merges prior DroppedCount/MinimalFallback/truncatedFields deterministically.
func TestPriorOversizeMergeCarriesDroppedAndMinimal(t *testing.T) {
	// The merge path is exercised end-to-end via BoundFailureDiagnosticsCarried
	// with a prior receipt that includes DroppedCount and MinimalFallback.
	prior := &runkernel.FailureTruncation{
		DiagnosticsTruncated: true,
		OmittedCount:         3,
		DroppedCount:         2,
		TruncatedFields:      []string{"message", "path"},
		SerializedBytes:      512,
		MinimalFallback:      false,
	}
	over := make([]runkernel.FailureDiagnostic, 0, runkernel.MaxFailureDiagnostics+2)
	for index := 0; index < runkernel.MaxFailureDiagnostics+2; index++ {
		over = append(over, runkernel.FailureDiagnostic{Code: "message.target_invalid", Field: "target", Message: "safe"})
	}
	bounded, truncation := runkernel.BoundFailureDiagnosticsCarried(over, prior)
	require.Len(t, bounded, runkernel.MaxFailureDiagnostics)
	require.NotNil(t, truncation)
	require.Equal(t, 5, truncation.OmittedCount, "prior 3 + fresh 2 omitted counts accumulate")
	require.Equal(t, 2, truncation.DroppedCount, "prior dropped count carries forward")
	require.Contains(t, truncation.TruncatedFields, "message")
	require.Contains(t, truncation.TruncatedFields, "path")
	require.False(t, truncation.MinimalFallback)
}
