package installation

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/app/registry"
)

// TestShareLockOrderAndDecidedProjection proves the definition-first lock
// order survives repeated decisions (the pending guard rejects a second
// decision without touching the request row first) and that a decided
// request still projects its full identity: app, installation, reason,
// timestamps, decision time, app name, and the reviewed policy snapshot
// including every extension's definitionId.
func TestShareLockOrderAndDecidedProjection(t *testing.T) {
	ctx := context.Background()
	db := startDB(t, ctx)
	svc := New(db, nil, Hooks{})
	now := time.Now().UTC()
	admin, _ := seedWorkspace(t, db, "svc_share_proj", now)
	owner := seedMember(t, db, admin.WorkspaceID, "svc_share_proj_owner", now)

	installation, err := svc.InstallForSelf(ctx, owner.UserID, admin.WorkspaceID, InstallParams{
		AppID: registry.BraveSearchAppID,
	})
	require.NoError(t, err)

	request, err := svc.RequestShare(ctx, owner.UserID, admin.WorkspaceID, installation.ID, "share it")
	require.NoError(t, err)
	decided, err := svc.DecideShare(ctx, admin.UserID, admin.WorkspaceID, request.ID, true, "approved")
	require.NoError(t, err)
	require.Equal(t, "approved", decided.Status)
	require.Equal(t, installation.ID, decided.InstallationID)
	require.Equal(t, installation.AppID, decided.AppID)
	require.Equal(t, "share it", decided.Reason)
	require.False(t, decided.CreatedAt.IsZero())
	require.NotNil(t, decided.DecidedAt)
	require.NotEmpty(t, decided.AppName)

	require.NotNil(t, decided.ReviewedPolicy, "decided request must keep the reviewed policy snapshot")
	require.Equal(t, installation.AppID, decided.ReviewedPolicy.DefinitionID)
	require.Equal(t, installation.InstalledVer, decided.ReviewedPolicy.Version)
	require.NotEmpty(t, decided.ReviewedPolicy.PolicyDigest)
	require.NotNil(t, decided.ReviewedPolicy.InstallationConfig)
	for _, extension := range decided.ReviewedPolicy.Extensions {
		require.NotEmpty(t, extension.DefinitionID, "extension snapshot must carry definitionId")
		require.NotEmpty(t, extension.Domain)
		require.NotEmpty(t, extension.Digest)
	}

	// A second decision is rejected by the pending guard without re-locking
	// the request row ahead of the definition scope.
	_, err = svc.DecideShare(ctx, admin.UserID, admin.WorkspaceID, request.ID, false, "again")
	require.Error(t, err)
	require.Contains(t, err.Error(), "already decided")
}

// TestNonMemberSurfacesStoreErrorContract proves access sentinel errors are
// mapped onto the store error contract at the service boundary (the API
// classifier recognizes storeerr only).
func TestNonMemberSurfacesStoreErrorContract(t *testing.T) {
	ctx := context.Background()
	db := startDB(t, ctx)
	svc := New(db, nil, Hooks{})
	now := time.Now().UTC()
	admin, _ := seedWorkspace(t, db, "svc_errmap", now)

	_, _, err := svc.requireManagement(ctx, "usr_svc_errmap_stranger", admin.WorkspaceID, "appinst_missing")
	require.Error(t, err)
	require.NotContains(t, err.Error(), "access.")
}

// TestShareApproveRejectsTamperedSnapshot proves approval recomputes the
// pinned digest from the exact persisted snapshot fields: mutating a
// snapshot field while keeping the digest string leaves the request pending.
func TestShareApproveRejectsTamperedSnapshot(t *testing.T) {
	ctx := context.Background()
	db := startDB(t, ctx)
	svc := New(db, nil, Hooks{})
	now := time.Now().UTC()
	admin, _ := seedWorkspace(t, db, "share_tamper", now)
	owner := seedMember(t, db, admin.WorkspaceID, "share_tamper_owner", now)

	installation, err := svc.InstallForSelf(ctx, owner.UserID, admin.WorkspaceID, InstallParams{AppID: registry.BraveSearchAppID})
	require.NoError(t, err)

	request, err := svc.RequestShare(ctx, owner.UserID, admin.WorkspaceID, installation.ID, "share once")
	require.NoError(t, err)

	// Tamper: change a persisted snapshot field but keep the digest string.
	mustExec(t, db, `
UPDATE service_installation_requests
SET requested_config = JSON_SET(requested_config, '$.installationConfig.tone', 'tampered')
WHERE id = $1
`, request.ID)

	_, err = svc.DecideShare(ctx, admin.UserID, admin.WorkspaceID, request.ID, true, "approve")
	require.Error(t, err)
	require.Contains(t, err.Error(), "App policy changed")

	var status string
	require.NoError(t, db.QueryRow(ctx, `
SELECT status
FROM service_installation_requests
WHERE id = $1
`, request.ID).Scan(&status))
	require.Equal(t, "pending", status, "tampered request must stay pending with no grant")

	var grants int
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*)
FROM service_principal_authorizations
WHERE service_installation_id = $1
  AND principal_kind = 'workspace'
  AND status = 'active'
`, installation.ID).Scan(&grants))
	require.Equal(t, 0, grants, "no workspace grant may be created from a tampered snapshot")
}
