package installation

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

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	installationstore "github.com/tidbcloud/AgentTeam/internal/store/app/installation"
	"github.com/tidbcloud/AgentTeam/internal/store/app/registry"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

// TestPersonalInstallationACLAndShare covers the service lifecycle: install
// as self, owner-only visibility, explicit grants, scope bounding, and the
// workspace share approval flow with digest fencing.
func TestPersonalInstallationACLAndShare(t *testing.T) {
	ctx := context.Background()
	db := startDB(t, ctx)
	svc := New(db, nil, Hooks{})
	now := time.Now().UTC()
	admin, _ := seedWorkspace(t, db, "svc_acl", now)
	owner := seedMember(t, db, admin.WorkspaceID, "svc_acl_owner", now)
	peer := seedMember(t, db, admin.WorkspaceID, "svc_acl_peer", now)

	installation, err := svc.InstallForSelf(ctx, owner.UserID, admin.WorkspaceID, InstallParams{
		AppID: registry.BraveSearchAppID,
	})
	require.NoError(t, err)
	require.Equal(t, serviceactor.OwnershipKindPersonal, installation.OwnershipKind)
	require.Equal(t, serviceactor.PersonalInstanceKeyPrefix+owner.MemberID, installation.InstanceKey)

	// Duplicate personal install conflicts with the store error contract.
	_, err = svc.InstallForSelf(ctx, owner.UserID, admin.WorkspaceID, InstallParams{
		AppID: registry.BraveSearchAppID,
	})
	require.Error(t, err)
	require.ErrorIs(t, err, storeerr.ErrConflict)

	// Ownership implies manage without an ACL row; the peer holds none.
	ownerManageErr := func() error {
		_, _, err := svc.requireManagement(ctx, owner.UserID, admin.WorkspaceID, installation.ID)
		return err
	}
	require.NoError(t, ownerManageErr(), "owner authority comes from ownership, not an ACL row")
	var ownerRows int
	require.NoError(t, db.QueryRow(ctx, "SELECT COUNT(*) FROM service_principal_authorizations WHERE service_installation_id = $1 AND principal_kind = 'human'", installation.ID).Scan(&ownerRows))
	require.Equal(t, 0, ownerRows, "no redundant owner ACL row is written")
	_, peerGranted, err := serviceactor.New(db).ResolveHumanInstallationAccess(ctx, admin.WorkspaceID, peer.MemberID, installation.ID)
	require.NoError(t, err)
	require.False(t, peerGranted)

	// Owner grants the peer human use access.
	granted, err := svc.GrantAccess(ctx, owner.UserID, admin.WorkspaceID, installation.ID, AccessGrantParams{
		PrincipalKind: serviceactor.PrincipalKindHuman,
		PrincipalRef:  peer.MemberID,
		AccessRole:    serviceactor.AccessRoleUse,
	})
	require.NoError(t, err)
	require.Equal(t, serviceactor.AccessRoleUse, granted.AccessRole)

	_, peerGrantedACL, errACL := serviceactor.New(db).ResolveHumanInstallationAccess(ctx, admin.WorkspaceID, peer.MemberID, installation.ID)
	require.NoError(t, errACL)
	require.True(t, peerGrantedACL)

	// Use access does not confer management: grant from the peer is denied.
	_, _, err = svc.requireManagement(ctx, peer.UserID, admin.WorkspaceID, installation.ID)
	require.Error(t, err)

	// Scope bounding: a scope outside the active grants is rejected.
	_, err = svc.GrantAccess(ctx, owner.UserID, admin.WorkspaceID, installation.ID, AccessGrantParams{
		PrincipalKind: serviceactor.PrincipalKindHuman,
		PrincipalRef:  peer.MemberID,
		AccessRole:    serviceactor.AccessRoleUse,
		Scopes:        []string{"not.an.active.scope"},
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "grant_scope_denied")

	// Revoke collapses visibility.
	accessRows, err := svc.ListAccess(ctx, owner.UserID, admin.WorkspaceID, installation.ID)
	require.NoError(t, err)
	peerGrantID := ""
	for _, row := range accessRows {
		if row.PrincipalKind == serviceactor.PrincipalKindHuman && row.PrincipalRef == peer.MemberID {
			peerGrantID = row.ID
		}
	}
	require.NotEmpty(t, peerGrantID)
	require.NoError(t, svc.RevokeAccess(ctx, owner.UserID, admin.WorkspaceID, installation.ID, peerGrantID))
	_, peerGrantedAfter, errAfter := serviceactor.New(db).ResolveHumanInstallationAccess(ctx, admin.WorkspaceID, peer.MemberID, installation.ID)
	require.NoError(t, errAfter)
	require.False(t, peerGrantedAfter)

	// Workspace principal grants are reserved for the share flow.
	_, err = svc.GrantAccess(ctx, owner.UserID, admin.WorkspaceID, installation.ID, AccessGrantParams{
		PrincipalKind: serviceactor.PrincipalKindWorkspace,
		PrincipalRef:  admin.WorkspaceID,
	})
	require.Error(t, err)

	// Share approval: owner submits, admin decides; approval creates the
	// workspace use authorization and peer visibility.
	shareRequest, err := svc.RequestShare(ctx, owner.UserID, admin.WorkspaceID, installation.ID, "team needs it")
	require.NoError(t, err)
	require.Equal(t, "pending", shareRequest.Status)

	var adminSharePending int
	require.NoError(t, db.QueryRow(ctx, "SELECT COUNT(*) FROM service_installation_requests WHERE workspace_id = $1 AND request_kind = 'share' AND status = 'pending'", admin.WorkspaceID).Scan(&adminSharePending))
	require.Equal(t, 1, adminSharePending)
	var peerSharePending int
	require.NoError(t, db.QueryRow(ctx, "SELECT COUNT(*) FROM service_installation_requests WHERE workspace_id = $1 AND requested_by_member_id = $2 AND request_kind = 'share' AND status = 'pending'", admin.WorkspaceID, peer.MemberID).Scan(&peerSharePending))
	require.Equal(t, 0, peerSharePending)

	decided, err := svc.DecideShare(ctx, admin.UserID, admin.WorkspaceID, shareRequest.ID, true, "approved")
	require.NoError(t, err)
	require.NotEqual(t, "pending", decided.Status)

	_, peerGrantedShare, errShare := serviceactor.New(db).ResolveHumanInstallationAccess(ctx, admin.WorkspaceID, peer.MemberID, installation.ID)
	require.NoError(t, errShare)
	require.True(t, peerGrantedShare)

	// Deciding twice fails.
	_, err = svc.DecideShare(ctx, admin.UserID, admin.WorkspaceID, shareRequest.ID, false, "already decided")
	require.Error(t, err)

	// A non-admin cannot decide.
	second, err := svc.RequestShare(ctx, owner.UserID, admin.WorkspaceID, installation.ID, "share again")
	require.NoError(t, err)
	_, err = svc.DecideShare(ctx, peer.UserID, admin.WorkspaceID, second.ID, true, "not admin")
	require.Error(t, err)
}

// TestInstallForSelfRejectsFeatureOwnedApps proves feature-owned setup flows
// (GitHub) cannot be bypassed by the personal install path.
func TestInstallForSelfRejectsFeatureOwnedApps(t *testing.T) {
	ctx := context.Background()
	db := startDB(t, ctx)
	svc := New(db, nil, Hooks{})
	now := time.Now().UTC()
	admin, _ := seedWorkspace(t, db, "svc_guard", now)
	member := seedMember(t, db, admin.WorkspaceID, "svc_guard_member", now)

	_, err := svc.InstallForSelf(ctx, member.UserID, admin.WorkspaceID, InstallParams{AppSlug: "github"})
	require.Error(t, err)
	require.Contains(t, err.Error(), "GitHub")
}

// TestProviderCapabilityToolsStayCanonical proves the single-sourced
// provider tool spec survives the real install/replay chains: for Gitea a
// real personal install, for GitHub the canonical install primitive, both
// reading the definition-wide mcp.tools capability before and after.
func TestProviderCapabilityToolsStayCanonical(t *testing.T) {
	wantTools := []string{"watch_pull_request", "list_watches", "pause_watch", "resume_watch", "unwatch"}
	wantScopes := []string{"service_event_subscription.read", "service_event_subscription.write"}
	readCapability := func(t *testing.T, db coredb.DB, definitionID string) (tools []string, scopes []string) {
		var configJSON []byte
		require.NoError(t, db.QueryRow(context.Background(), `
SELECT COALESCE(config, JSON_OBJECT())
FROM service_capabilities
WHERE service_definition_id = $1
  AND capability_key = 'mcp.tools'
LIMIT 1
`, definitionID).Scan(&configJSON))
		var config struct {
			Tools  []string `json:"tools"`
			Scopes []string `json:"scopes"`
		}
		require.NoError(t, json.Unmarshal(configJSON, &config))
		return config.Tools, config.Scopes
	}
	assertCapability := func(t *testing.T, db coredb.DB, definitionID string) {
		tools, scopes := readCapability(t, db, definitionID)
		require.Equal(t, wantTools, tools, definitionID+" tools")
		require.Equal(t, wantScopes, scopes, definitionID+" scopes")
	}

	t.Run("gitea real personal install", func(t *testing.T) {
		ctx := context.Background()
		db := startDB(t, ctx)
		svc := New(db, nil, Hooks{})
		now := time.Now().UTC()
		admin, _ := seedWorkspace(t, db, "cap_gitea", now)
		owner := seedMember(t, db, admin.WorkspaceID, "cap_gitea_owner", now)

		assertCapability(t, db, registry.GiteaAppID)
		personal, err := svc.InstallForSelf(ctx, owner.UserID, admin.WorkspaceID, InstallParams{AppSlug: registry.GiteaAppSlug})
		require.NoError(t, err)
		require.NotEmpty(t, personal.ID)
		assertCapability(t, db, registry.GiteaAppID)
	})

	t.Run("github canonical primitive", func(t *testing.T) {
		ctx := context.Background()
		db := startDB(t, ctx)
		now := time.Now().UTC()
		admin, _ := seedWorkspace(t, db, "cap_github", now)
		owner := seedMember(t, db, admin.WorkspaceID, "cap_github_owner", now)

		assertCapability(t, db, registry.GitHubAppID)
		err := coredb.WithTx(ctx, db, func(tx coredb.Tx) error {
			if err := installationstore.LockAppInstallationMutationScope(ctx, tx, registry.GitHubAppID); err != nil {
				return err
			}
			_, err := installationstore.InstallPreparedInstance(
				ctx, tx, serviceactor.TxStoreFrom(tx), admin.WorkspaceID, owner.MemberID,
				installationstore.PreparedInstance{
					DefinitionID:  registry.GitHubAppID,
					Version:       "1.0.0",
					VersionScopes: []string{"profile.read"},
					ConfigJSON:    []byte(`{}`),
				},
				installationstore.InstanceIdentity{
					InstanceKey: serviceactor.PersonalInstanceKeyPrefix + owner.MemberID,
					Handle:      installationstore.PersonalHandle(registry.GitHubAppSlug, owner.MemberID),
					DisplayName: "GitHub Personal",
				},
				installationstore.InstallCallbacks{},
				now,
			)
			return err
		})
		require.NoError(t, err)
		assertCapability(t, db, registry.GitHubAppID)
	})
}
