package tokenanalysis

import (
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

func TestCompareUsesFullLedgerOutsideOpportunityTopN(t *testing.T) {
	baseline := comparisonReport(1000, []BlockSummary{
		{BlockID: "platform.agentteam.core", DisplayName: "Core", Origin: "agentteam_policy", Control: "direct", Lifetime: "session_persistent", MaterializedEstimate: 600},
		{BlockID: "platform.identity", DisplayName: "Identity", Origin: "agentteam_policy", Control: "direct", Lifetime: "session_persistent", MaterializedEstimate: 200},
	})
	candidate := comparisonReport(700, []BlockSummary{
		{BlockID: "platform.agentteam.core", DisplayName: "Core", Origin: "agentteam_policy", Control: "direct", Lifetime: "session_persistent", MaterializedEstimate: 400},
		{BlockID: "platform.identity", DisplayName: "Identity", Origin: "agentteam_policy", Control: "direct", Lifetime: "session_persistent", MaterializedEstimate: 100},
	})
	baseline.Opportunities = []Opportunity{{BlockID: "platform.agentteam.core"}}
	candidate.Opportunities = []Opportunity{{BlockID: "platform.agentteam.core"}}

	comparison := Compare(baseline, candidate)

	require.Equal(t, "descriptive", comparison.State)
	require.Len(t, comparison.BlockDeltas, 2)
	require.Equal(t, "platform.agentteam.core", comparison.BlockDeltas[0].BlockID)
	require.Equal(t, int64(-200), comparison.BlockDeltas[0].DeltaTokens)
	require.Equal(t, "platform.identity", comparison.BlockDeltas[1].BlockID)
	require.Equal(t, int64(-100), comparison.BlockDeltas[1].DeltaTokens)
}

func TestCompareUsesRootInvocationCount(t *testing.T) {
	baseline := comparisonReport(1000, nil)
	candidate := comparisonReport(800, nil)
	baseline.Summary.RootModelInvocations = availableValue(10)
	candidate.Summary.RootModelInvocations = availableValue(8)

	comparison := Compare(baseline, candidate)
	require.Equal(t, int64(-2), *comparison.RootInvocationDelta)
}

func TestCompareAcceptsDifferentBuildsOnlyForOneDeclaredVariable(t *testing.T) {
	baseline := controlledComparisonReport("base-build", "platform.agentteam.exec", "warm", "success")
	candidate := controlledComparisonReport("candidate-build", "platform.agentteam.exec", "warm", "success")
	baseline.Summary.RuntimeInput = 1000
	baseline.Summary.RuntimeCacheRead = 800
	candidate.Summary.RuntimeInput = 700
	candidate.Summary.RuntimeCacheRead = 550

	comparison := Compare(baseline, candidate)
	require.Equal(t, "comparable", comparison.State)
	require.Equal(t, int64(-300), comparison.InputDelta)
	require.Equal(t, int64(-50), *comparison.UncachedInputDelta)
	require.Equal(t, int64(-250), *comparison.CacheReadDelta)
}

func TestCompareRejectsDifferentTaskPayloadsWithMatchingDeclaredMetadata(t *testing.T) {
	baseline := controlledComparisonReport("base-build", "platform.agentteam.exec", "warm", "success")
	candidate := controlledComparisonReport("candidate-build", "platform.agentteam.exec", "warm", "success")
	candidate.Comparability.TaskPayloadFingerprint = strings.Repeat("b", 32)

	comparison := Compare(baseline, candidate)

	require.Equal(t, "not_comparable", comparison.State)
	require.Equal(t, []string{"task_payload_mismatch"}, comparison.MismatchReasons)
	require.Nil(t, comparison.InputRatio)
}

func TestCompareRequiresTaskPayloadIdentityForControlledRun(t *testing.T) {
	baseline := controlledComparisonReport("base-build", "platform.agentteam.exec", "warm", "success")
	candidate := controlledComparisonReport("candidate-build", "platform.agentteam.exec", "warm", "success")
	baseline.Comparability.TaskPayloadFingerprint = ""
	candidate.Comparability.TaskPayloadFingerprint = ""

	require.Equal(t, "descriptive", Compare(baseline, candidate).State)
}

func TestCompareDoesNotTreatUnknownBuildAsControlled(t *testing.T) {
	baseline := controlledComparisonReport("", "platform.agentteam.exec", "warm", "success")
	candidate := controlledComparisonReport("candidate-build", "platform.agentteam.exec", "warm", "success")

	require.Equal(t, "descriptive", Compare(baseline, candidate).State)
}

func TestCompareUsesFrozenSnapshotForActiveRunButRejectsMismatchedCache(t *testing.T) {
	baseline := controlledComparisonReport("base-build", "platform.agentteam.exec", "cold", "success")
	candidate := controlledComparisonReport("candidate-build", "platform.agentteam.exec", "warm", "success")
	baseline.Scope = AnalysisScope{Kind: "run", RunState: "ended"}
	candidate.Scope = AnalysisScope{Kind: "run", RunState: "active"}

	comparison := Compare(baseline, candidate)
	require.Equal(t, "not_comparable", comparison.State)
	require.Equal(t, []string{"cache_state_mismatch"}, comparison.MismatchReasons)
	require.Nil(t, comparison.InputRatio)
}

func TestCompareAllowsSavedSnapshotsFromStillActiveRuns(t *testing.T) {
	baseline := controlledComparisonReport("base-build", "platform.agentteam.exec", "warm", "success")
	candidate := controlledComparisonReport("candidate-build", "platform.agentteam.exec", "warm", "success")
	baseline.Scope.RunState = "active"
	candidate.Scope.RunState = "active"

	require.Equal(t, "comparable", Compare(baseline, candidate).State)
}

func TestCompareRejectsIncompleteEvidence(t *testing.T) {
	baseline := controlledComparisonReport("base-build", "platform.agentteam.exec", "warm", "success")
	candidate := controlledComparisonReport("candidate-build", "platform.agentteam.exec", "warm", "success")
	candidate.DataQuality.State = StateLimited
	candidate.DataQuality.Truncated = true

	comparison := Compare(baseline, candidate)
	require.Equal(t, "not_comparable", comparison.State)
	require.Contains(t, comparison.MismatchReasons, "incomplete_evidence")
	require.Nil(t, comparison.InputRatio)
}

func TestCompareRejectsFailedOutcomeAndNonRunScope(t *testing.T) {
	baseline := controlledComparisonReport("base-build", "platform.agentteam.exec", "warm", "failure")
	candidate := controlledComparisonReport("candidate-build", "platform.agentteam.exec", "warm", "failure")
	baseline.Scope = AnalysisScope{Kind: "agent", RunState: "not_applicable"}
	candidate.Scope = AnalysisScope{Kind: "agent", RunState: "not_applicable"}

	comparison := Compare(baseline, candidate)
	require.Equal(t, "not_comparable", comparison.State)
	require.ElementsMatch(t, []string{"run_scope_required", "outcome_not_success"}, comparison.MismatchReasons)
}

func TestReadReportIsStrictAndBounded(t *testing.T) {
	dir := t.TempDir()
	report := Portable(comparisonReport(100, nil))
	data, err := json.Marshal(report)
	require.NoError(t, err)
	path := filepath.Join(dir, "report.json")
	require.NoError(t, os.WriteFile(path, data, 0o600))
	_, err = ReadReport(path)
	require.NoError(t, err)

	require.NoError(t, os.WriteFile(path, []byte(`{"schemaVersion":"agentteam-token-analysis/v1","unknown":true}`), 0o600))
	_, err = ReadReport(path)
	require.Error(t, err)

	require.NoError(t, os.WriteFile(path, []byte(strings.Repeat("x", MaxArtifactBytes+1)), 0o600))
	_, err = ReadReport(path)
	require.ErrorContains(t, err, "exceeds")
}

func TestValidatePortableReportRejectsForgedMetadataAndLocalFields(t *testing.T) {
	report := Portable(comparisonReport(100, []BlockSummary{{
		BlockID:              "platform.agentteam.core",
		DisplayName:          knownBlocks["platform.agentteam.core"].DisplayName,
		Origin:               knownBlocks["platform.agentteam.core"].Origin,
		Control:              knownBlocks["platform.agentteam.core"].Control,
		Lifetime:             "session_persistent",
		SuggestedAction:      knownBlocks["platform.agentteam.core"].SuggestedAction,
		RequiredValidation:   knownBlocks["platform.agentteam.core"].Validation,
		InjectionCoverage:    1,
		InjectionCount:       1,
		MaterializedEstimate: 100,
	}}))
	require.NoError(t, ValidatePortableReport(report))

	forged := report
	forged.Ledger = append([]BlockSummary(nil), report.Ledger...)
	forged.Ledger[0].DisplayName = "/tmp/private/source.go"
	forged.Fingerprint = reportFingerprint(forged)
	require.ErrorContains(t, ValidatePortableReport(forged), "non-resolved")

	local := report
	local.Agents = []AgentSummary{{
		AgentRef: "agent_0000000000000001",
	}}
	local.Fingerprint = reportFingerprint(local)
	require.ErrorContains(t, ValidatePortableReport(local), "machine-local")
}

func TestValidatePortableReportRejectsOversizedAndMalformedMetadata(t *testing.T) {
	report := Portable(comparisonReport(100, nil))

	oversized := report
	oversized.Comparability.ScopeFingerprint = strings.Repeat("x", 129)
	oversized.Fingerprint = reportFingerprint(oversized)
	require.ErrorContains(t, ValidatePortableReport(oversized), "invalid metadata")

	invalidPayloadFP := report
	invalidPayloadFP.Comparability.TaskPayloadFingerprint = "not-a-keyed-fingerprint"
	invalidPayloadFP.Fingerprint = reportFingerprint(invalidPayloadFP)
	require.ErrorContains(t, ValidatePortableReport(invalidPayloadFP), "invalid metadata")

	malformedRef := report
	malformedRef.Agents = []AgentSummary{{AgentRef: "agent_private"}}
	malformedRef.Fingerprint = reportFingerprint(malformedRef)
	require.ErrorContains(t, ValidatePortableReport(malformedRef), "Agent metadata")

	invalidQuality := report
	invalidQuality.DataQuality = DataQuality{State: StateLimited, Scanned: 2, Limit: 1}
	invalidQuality.Fingerprint = reportFingerprint(invalidQuality)
	require.ErrorContains(t, ValidatePortableReport(invalidQuality), "data-quality")

	invalidCache := report
	invalidCache.Summary.RuntimeCacheRead = invalidCache.Summary.RuntimeInput + 1
	invalidCache.Fingerprint = reportFingerprint(invalidCache)
	require.ErrorContains(t, ValidatePortableReport(invalidCache), "cache-read")
}

func TestValidatePortableReportRejectsForgedOpportunityAndDuplicates(t *testing.T) {
	metadata := knownBlocks["platform.agentteam.exec"]
	addressable := uint64(300)
	exposure := uint64(600)
	report := Portable(comparisonReport(100, []BlockSummary{{
		BlockID:              "platform.agentteam.exec",
		DisplayName:          metadata.DisplayName,
		Origin:               metadata.Origin,
		Control:              metadata.Control,
		Lifetime:             "session_persistent",
		InjectionCoverage:    1,
		InjectionCount:       1,
		MaterializedEstimate: 600,
		ExposureCount:        1,
		ExposureEstimate:     &exposure,
		ControllableExposure: &exposure,
		AddressableEstimate:  &addressable,
		SuggestedAction:      metadata.SuggestedAction,
		RequiredValidation:   metadata.Validation,
	}}))
	report.Opportunities = []Opportunity{{
		BlockID:            "platform.agentteam.exec",
		DisplayName:        metadata.DisplayName,
		Origin:             metadata.Origin,
		Control:            metadata.Control,
		Estimate:           301,
		Estimator:          "addressable_exposure_target",
		SuggestedAction:    metadata.SuggestedAction,
		RequiredValidation: metadata.Validation,
	}}
	report.Fingerprint = reportFingerprint(report)
	require.ErrorContains(t, ValidatePortableReport(report), "opportunity estimator")

	report.Opportunities = nil
	report.Ledger = append(report.Ledger, report.Ledger[0])
	report.Fingerprint = reportFingerprint(report)
	require.ErrorContains(t, ValidatePortableReport(report), "duplicate block")
}

func TestPortableReportDoesNotInventRuntimeFingerprintWithoutRuntimeEvidence(t *testing.T) {
	report := comparisonReport(100, nil)
	portable := Portable(report)

	require.Empty(t, portable.Comparability.RuntimeFingerprint)
	require.NoError(t, ValidatePortableReport(portable))
}

func TestReportFingerprintIgnoresCaptureTimeAndTransientCollectionAction(t *testing.T) {
	baseline := comparisonReport(100, nil)
	candidate := baseline
	candidate.GeneratedAt = baseline.GeneratedAt.Add(time.Hour)
	candidate.Collection.AutoEnabled = true
	candidate.Collection.Stopped = true

	require.Equal(t, reportFingerprint(baseline), reportFingerprint(candidate))
}

func comparisonReport(input uint64, ledger []BlockSummary) Report {
	snapshotAt := time.Unix(1_700_000_000, 0).UTC()
	report := Report{
		SchemaVersion: AnalysisSchemaVersion,
		GeneratedAt:   snapshotAt,
		SnapshotAt:    snapshotAt,
		Window:        "1d",
		Scope:         AnalysisScope{Kind: "all_agents", RunState: "not_applicable"},
		Collection:    CollectionStatus{CallerCanControl: true},
		Summary: UsageSummary{
			RuntimeInput:                  input,
			RootModelInvocations:          MetricValue{State: StateNoData},
			RootAverageInputPerInvocation: MetricValue{State: StateNoData},
			RootRuntimeInput:              MetricValue{State: StateNoData},
			UnattributedInput:             availableValue(input),
			CompiledMaterialized:          MetricValue{State: StateNoData},
			ControllableMaterialized:      MetricValue{State: StateNoData},
			ControllableExposure:          MetricValue{State: StateUnsupported},
			RequiredPayload:               MetricValue{State: StateNoData},
			ContextPartition:              unavailableContextPartition(StateNoData),
		},
		Agents:        []AgentSummary{},
		Runs:          []RunSummary{},
		Ledger:        ledger,
		Opportunities: []Opportunity{},
		DataQuality:   DataQuality{State: StateNoData},
		Comparability: ComparabilityInfo{
			CacheState:       "unknown",
			Outcome:          "unknown",
			ScopeFingerprint: "same-scope",
		},
	}
	report.Fingerprint = reportFingerprint(report)
	return report
}

func controlledComparisonReport(build, changedVariable, cache, outcome string) Report {
	report := comparisonReport(100, nil)
	report.Scope = AnalysisScope{Kind: "run", RunState: "ended"}
	report.DataQuality = DataQuality{State: StateAvailable}
	report.Comparability = ComparabilityInfo{
		CaseFingerprint:        "case",
		RuntimeFingerprint:     "runtime",
		BuildFingerprint:       build,
		WorkspaceFingerprint:   "workspace",
		ToolFingerprint:        "tools",
		TaskPayloadFingerprint: strings.Repeat("a", 32),
		ChangedVariable:        changedVariable,
		CacheState:             cache,
		Outcome:                outcome,
		ScopeFingerprint:       "scope",
	}
	report.Fingerprint = reportFingerprint(report)
	return report
}
