package exec

import (
	"context"
	"encoding/json"
	"os"
	"path/filepath"
	"testing"

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

func TestBuiltinActionInventoryMatchesCatalogAndDurableSpec(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	policies := builtinActionInventorySnapshot()
	require.Len(t, policies, catalog.len())
	require.NotEmpty(t, policies)

	keys := make(map[string]struct{}, len(policies))
	for _, policy := range policies {
		key := builtinActionKey(policy.Domain, policy.Name)
		require.NotContains(t, keys, key, "duplicate inventory key")
		keys[key] = struct{}{}
		descriptor, ok := catalog.lookup(policy.Domain, policy.Name)
		require.True(t, ok, "inventory action %s", key)
		require.Equal(t, string(descriptor.Permission), string(policy.Risk), key)
		require.NotEmpty(t, policy.Continuation, key)
		require.NotEmpty(t, policy.MutationReplay, key)
		require.NotEmpty(t, policy.PermissionExposure, key)
		require.NotEmpty(t, policy.StableIdentifiers, key)
		require.NotEmpty(t, policy.ErrorExit, key)
		require.NotEmpty(t, policy.HelpSchemaCompatibility, key)
		require.NotEmpty(t, policy.Rationale, key)
		require.NotEmpty(t, policy.AllowedSensitiveFields, key)
		require.NotEmpty(t, policy.RedactedSensitiveFields, key)
		require.NotEmpty(t, policy.StableSelectors, key)
		require.NotEmpty(t, policy.CanonicalFollowUp, key)
		require.NotEmpty(t, policy.ReplaySelector, key)
		require.NotEmpty(t, policy.ReplayDigest, key)
		require.NotEmpty(t, policy.ExpectedErrorClass, key)
		require.NotEmpty(t, policy.Retryability, key)
		require.NotEmpty(t, policy.HelpExampleStatus, key)
		require.NotEmpty(t, policy.CompatibilityDecision, key)
	}

	path := filepath.Join("..", "..", "..", "..", "specs", "daemon", "agentteam-exec-action-inventory.json")
	spec, err := os.ReadFile(path)
	require.NoError(t, err)
	var want, got any
	require.NoError(t, json.Unmarshal(spec, &want))
	encoded, err := json.MarshalIndent(policies, "", "  ")
	require.NoError(t, err)
	require.NoError(t, json.Unmarshal(encoded, &got))
	require.Equal(t, want, got, "durable Exec action inventory drifted from production policy")
}

func TestBuiltinActionInventoryRejectsMissingAuditDecision(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	decision, ok := builtinActionAuditDecisions["workspace list"]
	require.True(t, ok)
	delete(builtinActionAuditDecisions, "workspace list")
	t.Cleanup(func() { builtinActionAuditDecisions["workspace list"] = decision })
	err = validateBuiltinActionInventory(catalog.actionsSnapshot())
	require.Error(t, err)
	require.Contains(t, err.Error(), "lacks explicit audit decision")
}

func TestBuiltinActionInventoryRejectsMissingCoverage(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	descriptors := catalog.actionsSnapshot()
	descriptors = descriptors[:len(descriptors)-1]
	err = validateBuiltinActionInventory(descriptors)
	require.Error(t, err)
	require.Contains(t, err.Error(), "inventory has unregistered actions")
}

func TestBuiltinActionInventoryRejectsCursorInputSchemaDrift(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	descriptors := catalog.actionsSnapshot()
	for index := range descriptors {
		if descriptors[index].identity() == "project list" {
			descriptors[index].InputSchema = json.RawMessage(`{"type":"object","properties":{"limit":{"type":"integer"}}}`)
			break
		}
	}
	err = validateBuiltinActionInventory(descriptors)
	require.Error(t, err)
	require.Contains(t, err.Error(), "project list")
	require.Contains(t, err.Error(), "typed path field")
}

func TestBuiltinActionInventoryRejectsCursorReclassification(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	descriptor, ok := catalog.lookup("project", "list")
	require.True(t, ok)
	policy, ok := builtinActionPolicyFor("project", "list")
	require.True(t, ok)
	policy.ResultPolicy = builtinResultBounded
	err = validateBuiltinActionContinuationPolicy(policy, descriptor)
	require.Error(t, err)
	require.Contains(t, err.Error(), "must use the cursor result policy")
}

func TestBuiltinActionInventoryRejectsOneSidedCursorContract(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	descriptor, ok := catalog.lookup("project", "list")
	require.True(t, ok)
	descriptor.InputSchema = json.RawMessage(`{"type":"object","properties":{"limit":{"type":"integer"}}}`)
	descriptor.OutputSchema = json.RawMessage(`{"type":"object","properties":{"data":{"type":"object","properties":{"projects":{"type":"array"},"nextCursor":{"type":"string"}}}}}`)
	policy, ok := builtinActionPolicyFor("project", "list")
	require.True(t, ok)
	err = validateBuiltinActionContinuationPolicy(policy, descriptor)
	require.Error(t, err)
	require.Contains(t, err.Error(), "cursor input contract")
	require.Contains(t, err.Error(), "missing cursor property")
}

func TestBuiltinActionInventoryRejectsCursorOutputSchemaDrift(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	descriptors := catalog.actionsSnapshot()
	for index := range descriptors {
		if descriptors[index].identity() == "team list" {
			descriptors[index].OutputSchema = json.RawMessage(`{"type":"object","properties":{"data":{"type":"object","properties":{"teams":{"type":"array"},"hasMore":{"type":"boolean"}}}}}`)
			break
		}
	}
	err = validateBuiltinActionInventory(descriptors)
	require.Error(t, err)
	require.Contains(t, err.Error(), "team list cursor output contract")
	require.Contains(t, err.Error(), "missing nextCursor property")
}

func TestBuiltinActionInvokerCoverageRejectsMissingRoute(t *testing.T) {
	catalog, err := DefaultCatalog()
	require.NoError(t, err)
	invokers := make(map[string]builtinInvoker, catalog.len())
	capabilities := builtinRouteCapabilities()
	for _, descriptor := range catalog.actionsSnapshot() {
		invokers[descriptor.identity()] = builtinInvoker{
			invoke: func(_ context.Context, _ runtimeAuthorization, _ json.RawMessage) ([]byte, error) {
				return []byte(`{"data":{}}`), nil
			},
			capability: capabilities[descriptor.identity()],
		}
	}
	delete(invokers, catalog.actionsSnapshot()[0].identity())
	err = validateBuiltinActionInvokers(catalog, invokers)
	require.Error(t, err)
	require.Contains(t, err.Error(), "lacks route invoker")
}
