package tokenanalysis

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

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/metrics"
)

type evidenceStub struct {
	evidence metrics.TokenAnalysisEvidence
	queries  []metrics.TokenEvidenceQuery
}

func (stub *evidenceStub) QueryTokenAnalysisEvidence(query metrics.TokenEvidenceQuery) metrics.TokenAnalysisEvidence {
	stub.queries = append(stub.queries, query)
	return stub.evidence
}

func newServiceTestSubject(evidence metrics.TokenAnalysisEvidence) (*Service, *evidenceStub) {
	stub := &evidenceStub{evidence: evidence}
	service := New(Dependencies{
		Evidence: stub,
		Agents: func() []Agent {
			return []Agent{
				{ID: "agent-a", Name: "Alpha", Runtime: "codex", Model: "gpt-private"},
				{ID: "agent-b", Name: "Beta", Runtime: "claude", Model: "claude-private"},
			}
		},
		Now: func() time.Time { return time.Unix(1_700_000_000, 0) },
	})
	return service, stub
}

func TestCollectionIsOptInAndHumanAnalyzeControlsIt(t *testing.T) {
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })

	require.False(t, service.CollectionEnabled())
	passive, err := service.Analyze(Request{Window: "1d"}, true, false, "", false)
	require.NoError(t, err)
	require.False(t, passive.Collection.Enabled)
	require.False(t, passive.Collection.AutoEnabled)

	agentReport, err := service.Analyze(Request{Window: "1d"}, false, false, "agent-a", false)
	require.NoError(t, err)
	require.False(t, service.CollectionEnabled())
	require.Len(t, agentReport.Agents, 1)
	require.Equal(t, service.agentRef("agent-a"), agentReport.Agents[0].AgentRef)

	humanReport, err := service.Analyze(Request{Window: "1d"}, true, false, "", true)
	require.NoError(t, err)
	require.True(t, service.CollectionEnabled())
	require.True(t, humanReport.Collection.AutoEnabled)

	stopped, err := service.Analyze(Request{Window: "1d"}, true, true, "", true)
	require.NoError(t, err)
	require.True(t, stopped.Collection.Stopped)
	require.False(t, service.CollectionEnabled())

	alreadyStopped, err := service.Analyze(Request{Window: "1d"}, true, true, "", true)
	require.NoError(t, err)
	require.False(t, alreadyStopped.Collection.AutoEnabled)
	require.False(t, service.CollectionEnabled())
	require.Equal(t, StateDisabled, alreadyStopped.DataQuality.State)
	require.Contains(t, alreadyStopped.DataQuality.Diagnostics, "attribution_disabled")
	require.Contains(t, alreadyStopped.DataQuality.Diagnostics, "no_usage")
}

func TestDisabledReportRetainsHistoricalEvidenceButReportsCollectionState(t *testing.T) {
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{
		Usage: []metrics.UsageObservationRecord{{
			AgentID: "agent-a", Runtime: "codex", LaunchID: "root-1",
			SessionKind: "interactive", InputTokens: 100,
		}},
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })

	report, err := service.Analyze(Request{}, true, false, "", false)
	require.NoError(t, err)
	require.Equal(t, StateDisabled, report.DataQuality.State)
	require.Contains(t, report.DataQuality.Diagnostics, "attribution_disabled")
	require.Equal(t, uint64(100), *report.Summary.RootRuntimeInput.Value)
}

func TestReportIncludesConfiguredAgentsWithoutUsage(t *testing.T) {
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })

	report, err := service.Analyze(Request{}, true, false, "", false)
	require.NoError(t, err)
	require.Len(t, report.Agents, 2)
	require.Equal(t, []string{"Alpha", "Beta"}, []string{
		report.Agents[0].DisplayName,
		report.Agents[1].DisplayName,
	})
}

func TestCollectionChangeCallbackRunsOnlyOnTransitions(t *testing.T) {
	var changes []bool
	service := New(Dependencies{
		CollectionChanged: func(enabled bool) { changes = append(changes, enabled) },
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })

	require.True(t, service.SetCollectionEnabled(true))
	require.False(t, service.SetCollectionEnabled(true))
	require.True(t, service.SetCollectionEnabled(false))
	require.False(t, service.SetCollectionEnabled(false))
	require.Equal(t, []bool{true, false}, changes)
}

func TestAnalyzeScopesAgentCallerToSelf(t *testing.T) {
	service, stub := newServiceTestSubject(metrics.TokenAnalysisEvidence{})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })

	_, err := service.Analyze(Request{AgentRef: service.agentRef("agent-b")}, false, false, "agent-a", false)
	require.EqualError(t, err, "agent_ref_not_found")
	require.False(t, service.CollectionEnabled())

	_, err = service.Analyze(Request{}, false, false, "agent-a", false)
	require.NoError(t, err)
	require.Equal(t, []string{"agent-a"}, stub.queries[len(stub.queries)-1].AgentIDs)
}

func TestAnalysisFiltersRejectRawAgentAndRunIDs(t *testing.T) {
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{
		Usage: []metrics.UsageObservationRecord{{
			AgentID: "agent-a", LaunchID: "run-a", Runtime: "codex", SessionKind: "interactive",
		}},
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })

	_, err := service.Analyze(Request{AgentRef: "agent-a"}, false, false, "agent-a", false)
	require.EqualError(t, err, "agent_ref_not_found")
	_, err = service.Analyze(Request{RunRef: "run-a"}, false, false, "agent-a", false)
	require.EqualError(t, err, "run_ref_not_found")
}

func TestOpaqueRefsAreSaltedPerDaemonLifetime(t *testing.T) {
	previous := newRefKey
	var value byte
	newRefKey = func() [32]byte {
		value++
		var key [32]byte
		key[0] = value
		return key
	}
	t.Cleanup(func() { newRefKey = previous })

	first, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{})
	second, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{})
	require.NotEqual(t, first.agentRef("agent-a"), second.agentRef("agent-a"))
	require.NotEqual(t, first.runRef("run-a"), second.runRef("run-a"))
	require.Equal(t, scopeFingerprint("1d", "all_agents", 2), scopeFingerprint("1d", "all_agents", 2))
	require.NotEqual(t, scopeFingerprint("1d", "all_agents", 2), scopeFingerprint("1d", "agent", 1))
}

func TestRunRefResolutionUsesRequestedWindowAndAgentScope(t *testing.T) {
	service, stub := newServiceTestSubject(metrics.TokenAnalysisEvidence{
		Usage: []metrics.UsageObservationRecord{{
			AgentID: "agent-a", LaunchID: "run-a", Runtime: "codex", SessionKind: "interactive",
		}},
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })

	_, err := service.Analyze(Request{
		Window: "6h",
		RunRef: service.runRef("run-a"),
	}, false, false, "agent-a", false)
	require.NoError(t, err)
	require.GreaterOrEqual(t, len(stub.queries), 2)
	require.Equal(t, metrics.TimeWindow6Hours, stub.queries[0].Window)
	require.Equal(t, []string{"agent-a"}, stub.queries[0].AgentIDs)
	require.Equal(t, "run-a", stub.queries[1].LaunchID)
}

func TestRunFilterScopesTotalsAndAgentProjectionToMatchedRun(t *testing.T) {
	usage := metrics.NewServerMessageCollector(nil)
	usage.RecordAgentTokenUsage("agent-a", "gpt", 900, 90, 9)
	usage.RecordAgentTokenUsage("agent-b", "claude", 800, 80, 8)
	stub := &evidenceStub{evidence: metrics.TokenAnalysisEvidence{
		Usage: []metrics.UsageObservationRecord{{
			AgentID: "agent-b", Runtime: "claude", LaunchID: "run-b",
			SessionKind: "interactive", InputTokens: 120, CachedTokens: 12, OutputTokens: 6,
		}},
	}}
	service := New(Dependencies{
		Evidence: stub,
		Usage:    usage,
		Agents: func() []Agent {
			return []Agent{{ID: "agent-a", Name: "Alpha"}, {ID: "agent-b", Name: "Beta"}}
		},
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })
	service.SetCollectionEnabled(true)

	report, err := service.Analyze(Request{RunRef: service.runRef("run-b")}, true, false, "", false)
	require.NoError(t, err)
	require.Equal(t, uint64(120), report.Summary.RuntimeInput)
	require.Equal(t, uint64(12), report.Summary.RuntimeCacheRead)
	require.Equal(t, uint64(6), report.Summary.RuntimeOutput)
	require.Len(t, report.Agents, 1)
	require.Equal(t, service.agentRef("agent-b"), report.Agents[0].AgentRef)
	require.Equal(t, uint64(120), report.Agents[0].Input)
	require.Equal(t, []string{"agent-b"}, stub.queries[len(stub.queries)-1].AgentIDs)
	require.Equal(t, "run-b", stub.queries[len(stub.queries)-1].LaunchID)
}

func TestRunReportUsesOneSnapshotAndMarksActiveLaunch(t *testing.T) {
	startedAt := time.UnixMilli(1_700_000_000_123).UTC()
	now := startedAt.Add(time.Hour)
	stub := &evidenceStub{evidence: metrics.TokenAnalysisEvidence{
		Usage: []metrics.UsageObservationRecord{{
			LaunchStartedAt: startedAt.UnixMilli(),
			AgentID:         "agent-a",
			Runtime:         "codex",
			LaunchID:        "run-a",
			SessionKind:     "interactive",
			InputTokens:     42,
		}},
	}}
	service := New(Dependencies{
		Evidence:       stub,
		Agents:         func() []Agent { return []Agent{{ID: "agent-a"}} },
		ActiveLaunches: func() []string { return []string{"run-a"} },
		Now:            func() time.Time { return now },
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })
	service.SetCollectionEnabled(true)

	report, err := service.Analyze(Request{
		Window: "24h",
		RunRef: service.runRef("run-a"),
	}, false, false, "agent-a", false)
	require.NoError(t, err)
	require.Equal(t, "1d", report.Window)
	require.Equal(t, now, report.SnapshotAt)
	require.Equal(t, AnalysisScope{
		Kind: "run", RunState: "active", RunStartedAt: &startedAt,
	}, report.Scope)
	require.Len(t, report.Runs, 1)
	require.True(t, report.Runs[0].ActiveAtSnapshot)
	require.Equal(t, &startedAt, report.Runs[0].StartedAt)
	require.Len(t, stub.queries, 2)
	require.Equal(t, now.UnixMilli(), stub.queries[0].SnapshotAt)
	require.Equal(t, now.UnixMilli(), stub.queries[1].SnapshotAt)
	require.False(t, report.Collection.CallerCanControl)
}

func TestAgentDisabledReportExplainsHumanCollectionBoundary(t *testing.T) {
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })

	report, err := service.Analyze(Request{}, false, false, "agent-a", false)
	require.NoError(t, err)
	require.False(t, report.Collection.CallerCanControl)
	require.Contains(t, report.DataQuality.Diagnostics, "collection_enable_requires_human_owner")
}

func TestAnalyzeRejectsInvalidExperimentMetadata(t *testing.T) {
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })

	_, err := service.Analyze(Request{
		Experiment: ExperimentMetadata{CacheState: "mixed"},
	}, true, false, "", false)
	require.ErrorContains(t, err, "invalid cache state")

	_, err = service.Analyze(Request{
		Experiment: ExperimentMetadata{ChangedVariable: "platform.agentteam.exec"},
	}, true, false, "", false)
	require.EqualError(t, err, "changed_variable_requires_run")
}

func TestRunReportDerivesOnePrivacySafeTaskPayloadFP(t *testing.T) {
	taskPayloadFP := strings.Repeat("a", 32)
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{
		PromptRecords: []metrics.PromptCompileRecord{
			{AgentID: "agent-a", Runtime: "codex", LaunchID: "run-a", SessionKind: "interactive", Freshness: "fresh", CompileEvent: "wake", Stage: "turn_context", Slot: "turn", BlockID: "event.current_message", TaskPayloadPresent: true, TaskPayloadFP: taskPayloadFP, Tokens: 10},
			{AgentID: "agent-a", Runtime: "codex", LaunchID: "run-a", SessionKind: "interactive", Freshness: "fresh", CompileEvent: "wake", Stage: "turn_context", Slot: "turn", BlockID: "event.wake_context", TaskPayloadPresent: true, TaskPayloadFP: taskPayloadFP, Tokens: 20},
		},
		Usage: []metrics.UsageObservationRecord{{
			AgentID: "agent-a", LaunchID: "run-a", Runtime: "codex", SessionKind: "interactive",
		}},
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })
	service.SetCollectionEnabled(true)

	report, err := service.Analyze(Request{RunRef: service.runRef("run-a")}, true, false, "", false)
	require.NoError(t, err)
	require.Equal(t, taskPayloadFP, report.Comparability.TaskPayloadFingerprint)
	require.NotContains(t, report.Comparability.TaskPayloadFingerprint, "run-a")
}

func TestRunReportWithMultipleTaskPayloadsFailsClosed(t *testing.T) {
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{
		PromptRecords: []metrics.PromptCompileRecord{
			{LaunchID: "run-a", TaskPayloadPresent: true, TaskPayloadFP: strings.Repeat("a", 32)},
			{LaunchID: "run-a", TaskPayloadPresent: true, TaskPayloadFP: strings.Repeat("b", 32)},
		},
		Usage: []metrics.UsageObservationRecord{{
			AgentID: "agent-a", LaunchID: "run-a", Runtime: "codex", SessionKind: "interactive",
		}},
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })
	service.SetCollectionEnabled(true)

	report, err := service.Analyze(Request{RunRef: service.runRef("run-a")}, true, false, "", false)
	require.NoError(t, err)
	require.Empty(t, report.Comparability.TaskPayloadFingerprint)
}

func TestRunReportWithInvalidTaskPayloadIdentityFailsClosed(t *testing.T) {
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{
		PromptRecords: []metrics.PromptCompileRecord{{
			LaunchID: "run-a", TaskPayloadPresent: true, TaskPayloadFP: "not-a-keyed-fingerprint",
		}},
		Usage: []metrics.UsageObservationRecord{{
			AgentID: "agent-a", LaunchID: "run-a", Runtime: "codex", SessionKind: "interactive",
		}},
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })
	service.SetCollectionEnabled(true)

	report, err := service.Analyze(Request{RunRef: service.runRef("run-a")}, true, false, "", false)
	require.NoError(t, err)
	require.Empty(t, report.Comparability.TaskPayloadFingerprint)
}

func TestRunReportWithIdentifiedAndUnavailableTaskPayloadsFailsClosed(t *testing.T) {
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{
		PromptRecords: []metrics.PromptCompileRecord{
			{LaunchID: "run-a", TaskPayloadPresent: true, TaskPayloadFP: strings.Repeat("a", 32)},
			{LaunchID: "run-a", TaskPayloadPresent: true},
			{LaunchID: "run-a"}, // Non-payload attribution remains ignorable.
		},
		Usage: []metrics.UsageObservationRecord{{
			AgentID: "agent-a", LaunchID: "run-a", Runtime: "codex", SessionKind: "interactive",
		}},
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })
	service.SetCollectionEnabled(true)

	report, err := service.Analyze(Request{RunRef: service.runRef("run-a")}, true, false, "", false)
	require.NoError(t, err)
	require.Empty(t, report.Comparability.TaskPayloadFingerprint)
}

func TestOpportunityWithoutTargetReportsContributionNotAddressableSavings(t *testing.T) {
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{
		PromptRecords: []metrics.PromptCompileRecord{{
			LaunchID: "run-a", BlockID: "platform.agentteam.core", BlockLayer: "platform",
			Durability: "persistent", Tokens: 797,
		}},
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })
	service.SetCollectionEnabled(true)

	report, err := service.Analyze(Request{}, true, false, "", false)
	require.NoError(t, err)
	require.Len(t, report.Opportunities, 1)
	require.Equal(t, uint64(797), report.Opportunities[0].Estimate)
	require.Equal(t, "materialized_contribution", report.Opportunities[0].Estimator)
	require.Nil(t, report.Ledger[0].AddressableEstimate)
}

func TestReportUsesCorrelatedInvocationExposureForSupportedRuntime(t *testing.T) {
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{
		PromptRecords: []metrics.PromptCompileRecord{
			{LaunchID: "run-a", BlockID: "platform.agentteam.core", BlockLayer: "platform", Durability: "persistent", Tokens: 40},
			{LaunchID: "run-a", BlockID: "platform.agentteam.exec", BlockLayer: "platform", Durability: "persistent", Tokens: 95},
		},
		Usage: []metrics.UsageObservationRecord{{
			AgentID: "agent-a", Runtime: "opencode", LaunchID: "run-a",
			SessionKind: "interactive", SessionID: "session-a",
			InvocationID: "step-1", InputTokens: 200,
		}},
		ExposureCoverage: []metrics.ExposureCoverageRecord{{
			LaunchID: "run-a", Observations: 1, CompleteObservations: 1,
		}},
		BlockExposures: []metrics.BlockExposureRecord{
			{LaunchID: "run-a", BlockID: "platform.agentteam.core", Durability: "persistent", ExposureCount: 1, Tokens: 40},
			{LaunchID: "run-a", BlockID: "platform.agentteam.exec", Durability: "persistent", ExposureCount: 1, Tokens: 95},
		},
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })
	service.SetCollectionEnabled(true)

	report, err := service.Analyze(Request{}, true, false, "", false)
	require.NoError(t, err)
	require.Equal(t, StateAvailable, report.Summary.ControllableExposure.State)
	require.Equal(t, uint64(135), *report.Summary.ControllableExposure.Value)
	require.Len(t, report.Runs, 1)
	require.Equal(t, uint64(135), *report.Runs[0].ControllableExposure.Value)
	require.NotContains(t, report.DataQuality.Diagnostics, "exposure_correlation_unsupported")

	ledger := make(map[string]BlockSummary, len(report.Ledger))
	for _, block := range report.Ledger {
		ledger[block.BlockID] = block
	}
	require.Equal(t, uint64(40), *ledger["platform.agentteam.core"].ExposureEstimate)
	require.Equal(t, uint64(95), *ledger["platform.agentteam.exec"].ExposureEstimate)
	require.Equal(t, uint64(0), *ledger["platform.agentteam.exec"].AddressableEstimate)
	require.Len(t, report.Opportunities, 1)
	require.Equal(t, "platform.agentteam.core", report.Opportunities[0].BlockID)
	require.Equal(t, "exposure_contribution", report.Opportunities[0].Estimator)
}

func TestReportMarksExposureLimitedWhenAnyInvocationContextIsUnknown(t *testing.T) {
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{
		PromptRecords: []metrics.PromptCompileRecord{{
			LaunchID: "run-a", BlockID: "platform.agentteam.core", BlockLayer: "platform",
			Durability: "persistent", Tokens: 40,
		}},
		Usage: []metrics.UsageObservationRecord{{
			AgentID: "agent-a", Runtime: "kimi", LaunchID: "run-a",
			SessionKind: "interactive", InvocationID: "step-1", InputTokens: 200,
		}},
		ExposureCoverage: []metrics.ExposureCoverageRecord{{
			LaunchID: "run-a", Observations: 1,
		}},
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })
	service.SetCollectionEnabled(true)

	report, err := service.Analyze(Request{}, true, false, "", false)
	require.NoError(t, err)
	require.Equal(t, StateLimited, report.Summary.ControllableExposure.State)
	require.Equal(t, StateLimited, report.Runs[0].ControllableExposure.State)
	require.Contains(t, report.DataQuality.Diagnostics, "exposure_correlation_incomplete")
}

func TestReportMarksExposureLimitedWhenInvocationExposureIsMissing(t *testing.T) {
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{
		PromptRecords: []metrics.PromptCompileRecord{{
			LaunchID: "run-a", BlockID: "platform.agentteam.core", BlockLayer: "platform",
			Durability: "persistent", Tokens: 40,
		}},
		Usage: []metrics.UsageObservationRecord{
			{AgentID: "agent-a", Runtime: "opencode", LaunchID: "run-a", SessionKind: "interactive", SessionID: "session-a", InvocationID: "step-1", InputTokens: 100},
			{AgentID: "agent-a", Runtime: "opencode", LaunchID: "run-a", SessionKind: "interactive", SessionID: "session-a", InvocationID: "step-2", InputTokens: 100},
		},
		ExposureCoverage: []metrics.ExposureCoverageRecord{{
			LaunchID: "run-a", Observations: 1, CompleteObservations: 1,
		}},
		BlockExposures: []metrics.BlockExposureRecord{{
			LaunchID: "run-a", BlockID: "platform.agentteam.core", Durability: "persistent", ExposureCount: 1, Tokens: 40,
		}},
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })
	service.SetCollectionEnabled(true)

	report, err := service.Analyze(Request{}, true, false, "", false)
	require.NoError(t, err)
	require.Equal(t, StateLimited, report.Summary.ControllableExposure.State)
	require.Contains(t, report.DataQuality.Diagnostics, "exposure_correlation_incomplete")
}

func TestReportMarksMixedSupportedAndUnsupportedRunsLimited(t *testing.T) {
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{
		PromptRecords: []metrics.PromptCompileRecord{
			{LaunchID: "run-opencode", BlockID: "platform.agentteam.core", BlockLayer: "platform", Durability: "persistent", Tokens: 40},
			{LaunchID: "run-codex", BlockID: "platform.agentteam.core", BlockLayer: "platform", Durability: "persistent", Tokens: 40},
		},
		Usage: []metrics.UsageObservationRecord{
			{AgentID: "agent-a", Runtime: "opencode", LaunchID: "run-opencode", SessionKind: "interactive", SessionID: "session-a", InvocationID: "step-1", InputTokens: 100},
			{AgentID: "agent-a", Runtime: "codex", LaunchID: "run-codex", SessionKind: "interactive", SessionID: "session-b", InputTokens: 100},
		},
		ExposureCoverage: []metrics.ExposureCoverageRecord{{
			LaunchID: "run-opencode", Observations: 1, CompleteObservations: 1,
		}},
		BlockExposures: []metrics.BlockExposureRecord{{
			LaunchID: "run-opencode", BlockID: "platform.agentteam.core", Durability: "persistent", ExposureCount: 1, Tokens: 40,
		}},
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })
	service.SetCollectionEnabled(true)

	report, err := service.Analyze(Request{}, true, false, "", false)
	require.NoError(t, err)
	require.Equal(t, StateLimited, report.Summary.ControllableExposure.State)
	require.Contains(t, report.DataQuality.Diagnostics, "exposure_correlation_incomplete")
	runs := make(map[string]RunSummary, len(report.Runs))
	for _, run := range report.Runs {
		runs[run.Runtime] = run
	}
	require.Equal(t, StateAvailable, runs["opencode"].ControllableExposure.State)
	require.Equal(t, StateUnsupported, runs["codex"].ControllableExposure.State)
}

func TestReportIgnoresExposureExcludedInvocationWhenCheckingCoverage(t *testing.T) {
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{
		PromptRecords: []metrics.PromptCompileRecord{{
			LaunchID: "run-a", BlockID: "platform.agentteam.core", BlockLayer: "platform",
			Durability: "persistent", Tokens: 40,
		}},
		Usage: []metrics.UsageObservationRecord{
			{AgentID: "agent-a", Runtime: "kimi", LaunchID: "run-a", SessionKind: "interactive", SessionID: "session-a", InvocationID: "root-step", InputTokens: 100},
			{AgentID: "agent-a", Runtime: "kimi", LaunchID: "run-a", SessionKind: "interactive", SessionID: "session-a", InvocationID: "subagent-step", ExposureExcluded: true, InputTokens: 100},
		},
		ExposureCoverage: []metrics.ExposureCoverageRecord{{
			LaunchID: "run-a", Observations: 1, CompleteObservations: 1,
		}},
		BlockExposures: []metrics.BlockExposureRecord{{
			LaunchID: "run-a", BlockID: "platform.agentteam.core", Durability: "persistent", ExposureCount: 1, Tokens: 40,
		}},
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })
	service.SetCollectionEnabled(true)

	report, err := service.Analyze(Request{}, true, false, "", false)
	require.NoError(t, err)
	require.Equal(t, StateAvailable, report.Summary.ControllableExposure.State)
	require.Equal(t, uint64(40), *report.Summary.ControllableExposure.Value)
	require.Equal(t, StateAvailable, report.Runs[0].ControllableExposure.State)
}

func TestReportAggregatesRootInvocationsAndControllableContext(t *testing.T) {
	evidence := metrics.TokenAnalysisEvidence{
		Usage: []metrics.UsageObservationRecord{
			{AgentID: "agent-a", Runtime: "codex", LaunchID: "root-1", SessionKind: "interactive", SessionID: "thread-1", Model: "gpt", InvocationID: "turn-1", InputTokens: 100, CachedTokens: 20, OutputTokens: 10},
			{AgentID: "agent-a", Runtime: "codex", LaunchID: "root-1", SessionKind: "interactive", SessionID: "thread-1", Model: "gpt", InvocationID: "turn-1", InputTokens: 50, CachedTokens: 5, OutputTokens: 4},
			{AgentID: "agent-a", Runtime: "codex", LaunchID: "root-2", SessionKind: "interactive", SessionID: "thread-2", Model: "gpt", InvocationID: "turn-2", InputTokens: 80, CachedTokens: 8, OutputTokens: 6},
		},
		PromptRecords: []metrics.PromptCompileRecord{
			{LaunchID: "root-1", EventID: "event-1", BlockID: "platform.agentteam.exec", BlockLayer: "platform", Durability: "persistent", Records: 1, Tokens: 600},
			{LaunchID: "root-1", EventID: "event-1", BlockID: "event.current_message", BlockLayer: "event", Durability: "turn_only", Records: 1, Tokens: 900},
			{LaunchID: "root-1", EventID: "event-1", BlockID: "runtime_attachment_manifest", BlockLayer: "event", Durability: "turn_only", Records: 1, Tokens: 100},
			{LaunchID: "root-2", EventID: "event-2", BlockID: "event.current_message", BlockLayer: "event", Durability: "turn_only", Records: 1, Tokens: 700},
		},
	}
	service, _ := newServiceTestSubject(evidence)
	t.Cleanup(func() { service.SetCollectionEnabled(false) })
	service.SetCollectionEnabled(true)

	report, err := service.Analyze(Request{Window: "1d"}, true, false, "", false)
	require.NoError(t, err)
	require.Equal(t, uint64(230), *report.Summary.RootRuntimeInput.Value)
	require.Equal(t, uint64(2), *report.Summary.RootModelInvocations.Value)
	require.Equal(t, uint64(115), *report.Summary.RootAverageInputPerInvocation.Value)
	require.Equal(t, uint64(600), *report.Summary.ControllableMaterialized.Value)
	require.Equal(t, uint64(1700), *report.Summary.RequiredPayload.Value)
	require.Equal(t, uint64(600), *report.Summary.ContextPartition.AgentTeamOwned.Value)
	require.Equal(t, uint64(0), *report.Summary.ContextPartition.WorkspaceConfiguration.Value)
	require.Equal(t, uint64(1700), *report.Summary.ContextPartition.UserTaskPayload.Value)
	require.Equal(t, uint64(0), *report.Summary.ContextPartition.Unknown.Value)
	require.Len(t, report.Runs, 2)
	runs := make(map[uint64]RunSummary, len(report.Runs))
	for _, run := range report.Runs {
		require.Equal(t, "root", run.Kind)
		runs[run.Input] = run
	}
	require.Equal(t, uint64(1600), *runs[150].CompiledMaterialized.Value)
	require.Equal(t, uint64(600), *runs[150].ControllableMaterialized.Value)
	require.Equal(t, uint64(1000), *runs[150].RequiredPayloadMaterialized.Value)
	require.Equal(t, uint64(150), *runs[150].AverageInputPerInvocation.Value)
	require.Equal(t, uint64(700), *runs[80].CompiledMaterialized.Value)
	require.Equal(t, uint64(0), *runs[80].ControllableMaterialized.Value)
	require.Equal(t, uint64(700), *runs[80].RequiredPayloadMaterialized.Value)
	require.Equal(t, uint64(80), *runs[80].AverageInputPerInvocation.Value)
	require.Len(t, report.Ledger, 3)
	require.Len(t, report.Opportunities, 1)
	require.Equal(t, "platform.agentteam.exec", report.Opportunities[0].BlockID)
	require.Equal(t, uint64(600), report.Opportunities[0].Estimate)
	require.Equal(t, "materialized_contribution", report.Opportunities[0].Estimator)
}

func TestInvocationCountIsUnsupportedWithoutUniqueIdentity(t *testing.T) {
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{
		Usage: []metrics.UsageObservationRecord{{
			AgentID: "agent-a", Runtime: "claude", LaunchID: "root-1",
			SessionKind: "interactive", InputTokens: 100,
		}},
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })
	service.SetCollectionEnabled(true)

	report, err := service.Analyze(Request{}, true, false, "", false)
	require.NoError(t, err)
	require.Equal(t, StateUnsupported, report.Summary.RootModelInvocations.State)
	require.Equal(t, StateUnsupported, report.Summary.RootAverageInputPerInvocation.State)
	require.Contains(t, report.DataQuality.Diagnostics, "root_invocation_identity_unsupported")
}

func TestRunInvocationCountIsUnsupportedWhenOnlySomeUsageHasIdentity(t *testing.T) {
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{
		Usage: []metrics.UsageObservationRecord{
			{
				AgentID: "agent-a", Runtime: "codex", LaunchID: "root-1",
				SessionKind: "interactive", SessionID: "thread-1",
				InvocationID: "turn-1", InputTokens: 100,
			},
			{
				AgentID: "agent-a", Runtime: "claude", LaunchID: "root-1",
				SessionKind: "interactive", InputTokens: 50,
			},
		},
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })
	service.SetCollectionEnabled(true)

	report, err := service.Analyze(Request{}, true, false, "", false)
	require.NoError(t, err)
	require.Equal(t, StateUnsupported, report.Summary.RootModelInvocations.State)
	require.Len(t, report.Runs, 1)
	require.Equal(t, StateUnsupported, report.Runs[0].ModelInvocations.State)
}

func TestRepeatedUsageUpdatesForOneInvocationAreCountedOnce(t *testing.T) {
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{
		Usage: []metrics.UsageObservationRecord{
			{
				AgentID: "agent-a", Runtime: "codex", LaunchID: "root-1",
				SessionKind: "interactive", SessionID: "thread-1",
				InvocationID: "turn-1", InputTokens: 100,
			},
			{
				AgentID: "agent-a", Runtime: "codex", LaunchID: "root-1",
				SessionKind: "interactive", SessionID: "thread-1",
				InvocationID: "turn-1", InputTokens: 50,
			},
		},
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })
	service.SetCollectionEnabled(true)

	report, err := service.Analyze(Request{}, true, false, "", false)
	require.NoError(t, err)
	require.Equal(t, uint64(1), *report.Summary.RootModelInvocations.Value)
	require.Equal(t, uint64(150), *report.Summary.RootAverageInputPerInvocation.Value)
	require.Len(t, report.Runs, 1)
	require.Equal(t, uint64(1), *report.Runs[0].ModelInvocations.Value)
	require.Equal(t, uint64(150), *report.Runs[0].AverageInputPerInvocation.Value)
}

func TestUnmatchedLowCardinalityUsageIsLimitedInsteadOfPretendingToBeAThirdRun(t *testing.T) {
	usage := metrics.NewServerMessageCollector(nil)
	usage.RecordAgentTokenUsage("agent-a", "gpt", 250, 20, 10)
	service := New(Dependencies{
		Evidence: &evidenceStub{evidence: metrics.TokenAnalysisEvidence{
			Usage: []metrics.UsageObservationRecord{{
				AgentID: "agent-a", Runtime: "opencode", LaunchID: "root-1",
				SessionKind: "interactive", SessionID: "session-1",
				InvocationID: "step-1", InputTokens: 100,
			}},
		}},
		Usage:  usage,
		Agents: func() []Agent { return []Agent{{ID: "agent-a"}} },
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })
	service.SetCollectionEnabled(true)

	report, err := service.Analyze(Request{}, true, false, "", false)
	require.NoError(t, err)
	require.Equal(t, StateLimited, report.DataQuality.State)
	require.Contains(t, report.DataQuality.Diagnostics, "usage_correlation_incomplete")
	require.Equal(t, uint64(150), *report.Summary.UnattributedInput.Value)
	require.Len(t, report.Runs, 1)
}

func TestZeroValuedObservationKeepsInvocationMetricsUnsupported(t *testing.T) {
	service, _ := newServiceTestSubject(metrics.TokenAnalysisEvidence{
		Usage: []metrics.UsageObservationRecord{{
			AgentID: "agent-a", Runtime: "opencode", LaunchID: "root-1",
			SessionKind: "interactive", SessionID: "session-1",
		}},
	})
	t.Cleanup(func() { service.SetCollectionEnabled(false) })
	service.SetCollectionEnabled(true)

	report, err := service.Analyze(Request{}, true, false, "", false)
	require.NoError(t, err)
	require.Equal(t, StateUnsupported, report.Summary.RootModelInvocations.State)
	require.Equal(t, StateUnsupported, report.Summary.RootAverageInputPerInvocation.State)
}

func TestPortableReportRemovesLocalNamesAndExactRuntime(t *testing.T) {
	report := Report{
		SchemaVersion: AnalysisSchemaVersion,
		GeneratedAt:   time.Unix(1_700_000_000, 0).UTC(),
		SnapshotAt:    time.Unix(1_700_000_000, 0).UTC(),
		Window:        "1d",
		Scope:         AnalysisScope{Kind: "all_agents", RunState: "not_applicable"},
		Agents: []AgentSummary{{
			AgentRef: "agent_0000000000000001", DisplayName: "private", Runtime: "codex", Model: "gpt-private",
		}},
		Runs: []RunSummary{{
			RunRef: "run_0000000000000001", AgentRef: "agent_0000000000000001",
			Kind: "root", Runtime: "codex", Model: "gpt-private",
		}},
		Ledger:        []BlockSummary{},
		Opportunities: []Opportunity{},
		Comparability: ComparabilityInfo{CacheState: "unknown", Outcome: "unknown", ScopeFingerprint: "scope"},
	}
	report.Fingerprint = reportFingerprint(report)

	portable := Portable(report)
	encoded, err := json.Marshal(portable)
	require.NoError(t, err)
	require.NotContains(t, string(encoded), "private")
	require.NotContains(t, string(encoded), "codex")
	require.NotContains(t, string(encoded), "gpt-private")
	require.Empty(t, portable.Agents)
	require.Empty(t, portable.Runs)
	require.Len(t, portable.Comparability.RuntimeFingerprint, 16)
}
