package serviceactor

import (
	"context"
	"database/sql"
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
)

type targetBindingIntegritySeed struct {
	definition   Definition
	installation Installation
	member       Member
	params       TargetBindingUpsert
}

func TestServiceAccountTargetRefUsesStableAccountID(t *testing.T) {
	require.Equal(t, "service-account:sacct_AgentTeam-Official", ServiceAccountTargetRef(" sacct_AgentTeam-Official "))
}

func TestTargetBindingRejectsCrossTenantAndWrongTypeSubstrates(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := startServiceActorRollbackDB(t, ctx)
	now := time.Date(2026, 7, 10, 4, 0, 0, 0, time.UTC)
	firstFixture := seedServiceActorFixture(t, db, "binding_integrity_first", now)
	secondFixture := seedServiceActorFixture(t, db, "binding_integrity_second", now)
	store := New(db)
	first := seedTargetBindingIntegrityApp(t, ctx, store, firstFixture, "first", now)
	second := seedTargetBindingIntegrityApp(t, ctx, store, secondFixture, "second", now)

	wrongTypeScopeID := "scp_svcactor_binding_integrity_wrong_type"
	mustExec(t, db, `
INSERT INTO scopes (id, workspace_id, type, identity_key, status, created_by_member_id, created_at, updated_at)
VALUES ($1, $2, 'task', $3, 'active', $4, $5, $5)
`, wrongTypeScopeID, firstFixture.WorkspaceID, "task:"+wrongTypeScopeID, firstFixture.HumanMemberID, now)

	tests := []struct {
		name   string
		mutate func(*TargetBindingUpsert)
	}{
		{
			name: "installation workspace",
			mutate: func(params *TargetBindingUpsert) {
				params.ServiceInstallationID = second.installation.ID
				params.ServiceMemberID = second.member.ID
			},
		},
		{
			name: "definition relationship",
			mutate: func(params *TargetBindingUpsert) {
				params.ServiceDefinitionID = second.definition.ID
			},
		},
		{
			name: "definition kind",
			mutate: func(params *TargetBindingUpsert) {
				params.ServiceKind = "fixture"
			},
		},
		{
			name: "service member workspace",
			mutate: func(params *TargetBindingUpsert) {
				params.ServiceMemberID = second.member.ID
			},
		},
		{
			name: "authority subject workspace",
			mutate: func(params *TargetBindingUpsert) {
				params.AuthoritySubjectMemberID = secondFixture.HumanMemberID
			},
		},
		{
			name: "creator workspace",
			mutate: func(params *TargetBindingUpsert) {
				params.CreatedByMemberID = secondFixture.HumanMemberID
			},
		},
		{
			name: "delivery scope workspace",
			mutate: func(params *TargetBindingUpsert) {
				params.DeliveryScopeID = secondFixture.ScopeID
			},
		},
		{
			name: "permission scope workspace",
			mutate: func(params *TargetBindingUpsert) {
				params.PermissionCheckScopeID = secondFixture.ScopeID
			},
		},
		{
			name: "delivery scope type",
			mutate: func(params *TargetBindingUpsert) {
				params.DeliveryScopeID = wrongTypeScopeID
			},
		},
		{
			name: "permission scope type",
			mutate: func(params *TargetBindingUpsert) {
				params.PermissionCheckScopeID = wrongTypeScopeID
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			params := first.params
			test.mutate(&params)
			err := upsertTargetBindingInTransaction(ctx, db, store, params)
			require.ErrorIs(t, err, ErrConflict)
			requireTargetBindingCount(t, ctx, db, 0, first.params.ActiveBindingKey)
		})
	}

	require.NoError(t, upsertTargetBindingInTransaction(ctx, db, store, first.params))
	requireTargetBindingCount(t, ctx, db, 1, first.params.ActiveBindingKey)
}

func TestTargetBindingActiveAppReplayConvergesWithoutConflict(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := startServiceActorRollbackDB(t, ctx)
	now := time.Date(2026, 7, 10, 4, 5, 0, 0, time.UTC)
	fixture := seedServiceActorFixture(t, db, "binding_integrity_replay", now)
	store := New(db)
	seed := seedTargetBindingIntegrityApp(t, ctx, store, fixture, "replay", now)

	secondScopeID := "scp_svcactor_binding_integrity_replay_second"
	mustExec(t, db, `
INSERT INTO scopes (id, workspace_id, type, identity_key, status, created_by_member_id, created_at, updated_at)
VALUES ($1, $2, 'channel', $3, 'active', $4, $5, $5)
`, secondScopeID, fixture.WorkspaceID, "channel:"+secondScopeID, fixture.HumanMemberID, now)
	secondHumanMemberID := "mem_svcactor_binding_integrity_replay_second_human"
	secondUserID := "usr_svcactor_binding_integrity_replay_second_human"
	mustExec(t, db, `
INSERT INTO users (id, email, name, display_name, password_hash, created_at, updated_at)
VALUES ($1, $2, $3, $3, 'hash', $4, $4)
`, secondUserID, "binding-integrity-replay-second@example.com", "binding-integrity-replay-second", now)
	mustExec(t, db, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES ($1, $2, 'human', 'Second Human', $3, 'member', 'active', $4, $4)
`, secondHumanMemberID, fixture.WorkspaceID, secondUserID, now)

	require.NoError(t, upsertTargetBindingInTransaction(ctx, db, store, seed.params))
	original, err := loadTargetBindingByID(ctx, db, seed.params.ID)
	require.NoError(t, err)

	compatibleReplay := seed.params
	compatibleReplay.ID = "svctb_binding_integrity_replay_unused"
	compatibleReplay.DeliveryContract = []byte("{\n  \"mode\": \"app\", \"version\": 1\n}")
	compatibleReplay.ScopeSnapshot = []byte(`{"changed":"ignored-on-active-replay"}`)
	compatibleReplay.Metadata = []byte(`{"changed":"ignored-on-active-replay"}`)
	compatibleReplay.CreatedByActorID = "different-replay-actor"
	var replayed TargetBinding
	require.NoError(t, coredb.WithTx(ctx, db, func(tx coredb.Tx) error {
		var replayErr error
		replayed, replayErr = store.InTx(tx).UpsertTargetBinding(ctx, compatibleReplay)
		return replayErr
	}))
	require.Equal(t, original.ID, replayed.ID)
	require.JSONEq(t, string(compatibleReplay.Metadata), string(replayed.Metadata))
	require.JSONEq(t, string(compatibleReplay.ScopeSnapshot), string(replayed.ScopeSnapshot))
	appPrincipalReplay := seed.params
	appPrincipalReplay.AuthoritySubjectMemberID = secondHumanMemberID
	appPrincipalReplay.DeliveryContract = []byte(`{"mode":"principal-specific","version":2}`)
	require.NoError(t, upsertTargetBindingInTransaction(ctx, db, store, appPrincipalReplay))
	appConverged, err := loadTargetBindingByID(ctx, db, original.ID)
	require.NoError(t, err)
	require.Equal(t, secondHumanMemberID, appConverged.AuthoritySubjectMemberID)
	require.JSONEq(t, string(appPrincipalReplay.DeliveryContract), string(appConverged.DeliveryContract))

	tests := []struct {
		name         string
		mutate       func(*TargetBindingUpsert)
		wantConflict bool
		check        func(*testing.T, TargetBinding)
	}{
		{
			name: "delivery scope",
			mutate: func(params *TargetBindingUpsert) {
				params.DeliveryScopeID = secondScopeID
			},
			check: func(t *testing.T, binding TargetBinding) {
				require.Equal(t, secondScopeID, binding.DeliveryScopeID)
			},
		},
		{
			name: "permission scope",
			mutate: func(params *TargetBindingUpsert) {
				params.PermissionCheckScopeID = secondScopeID
			},
			check: func(t *testing.T, binding TargetBinding) {
				require.Equal(t, secondScopeID, binding.PermissionCheckScopeID)
			},
		},
		{
			name: "active binding key",
			mutate: func(params *TargetBindingUpsert) {
				params.ActiveBindingKey = "binding-integrity-replay-other-key"
			},
			check: func(t *testing.T, binding TargetBinding) {
				require.Equal(t, seed.params.ActiveBindingKey, binding.ActiveBindingKey)
			},
		},
		{
			name: "target ref through active key collision",
			mutate: func(params *TargetBindingUpsert) {
				params.ID = "svctb_binding_integrity_replay_other_target"
				params.TargetRef = "chn_binding_integrity_replay_other"
			},
			wantConflict: true,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			params := seed.params
			test.mutate(&params)
			err := upsertTargetBindingInTransaction(ctx, db, store, params)
			if test.wantConflict {
				require.ErrorIs(t, err, ErrConflict)
				return
			}
			require.NoError(t, err)
			converged, loadErr := loadTargetBindingByID(ctx, db, original.ID)
			require.NoError(t, loadErr)
			test.check(t, converged)
			requireTargetBindingCount(t, ctx, db, 1, seed.params.ActiveBindingKey)
		})
	}
}

func TestServiceAccountTargetBindingRequiresItsServiceMemberAuthority(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := startServiceActorRollbackDB(t, ctx)
	now := time.Date(2026, 7, 10, 4, 10, 0, 0, time.UTC)
	fixture := seedServiceActorFixture(t, db, "binding_integrity_service_account", now)
	store := New(db)
	definition, err := store.LoadDefinitionBySlug(ctx, KindServiceAccount, "service-account")
	require.NoError(t, err)
	installation, err := store.UpsertInstallation(ctx, InstallationUpsert{
		ID:                  "svcinst_binding_integrity_service_account",
		WorkspaceID:         fixture.WorkspaceID,
		ServiceDefinitionID: definition.ID,
		ServiceKind:         definition.ServiceKind,
		InstanceKey:         "sacct_binding_integrity_service_account",
		Status:              StatusActive,
		CreatedByMemberID:   fixture.HumanMemberID,
		Now:                 now,
	})
	require.NoError(t, err)
	member, err := store.UpsertMember(ctx, MemberUpsert{
		ID:                    "svcmem_binding_integrity_service_account",
		WorkspaceID:           fixture.WorkspaceID,
		ServiceDefinitionID:   definition.ID,
		ServiceInstallationID: installation.ID,
		MemberID:              fixture.ServiceMemberID,
		ServiceKind:           definition.ServiceKind,
		Handle:                "binding-integrity-service-account",
		DisplayName:           "Binding Integrity Service Account",
		Status:                StatusActive,
		Now:                   now,
	})
	require.NoError(t, err)
	serviceAccountScopeID := "scp_svcactor_binding_integrity_service_account_messages"
	mustExec(t, db, `
INSERT INTO scopes (id, workspace_id, type, identity_key, status, created_by_member_id, created_at, updated_at)
VALUES ($1, $2, 'service_account', $3, 'active', $4, $5, $5)
`, serviceAccountScopeID, fixture.WorkspaceID, "service_account:"+serviceAccountScopeID, fixture.HumanMemberID, now)
	serviceAccountID := "sacct_binding_integrity_service_account"
	mustExec(t, db, `
INSERT INTO service_accounts (
    id, principal_id, workspace_id, management_kind,
    service_installation_id, service_member_id, author_member_id, message_scope_id,
    name, description, created_by_member_id, created_at, updated_at
) VALUES (
    $1, $1, $2, 'workspace',
    $3, $4, $5, $6,
    'Binding Integrity Service Account', '', $7, $8, $8
)
`, serviceAccountID, fixture.WorkspaceID, installation.ID, member.ID, fixture.ServiceMemberID, serviceAccountScopeID, fixture.HumanMemberID, now)

	params := TargetBindingUpsert{
		ID:                       "svctb_binding_integrity_service_account",
		WorkspaceID:              fixture.WorkspaceID,
		ServiceDefinitionID:      definition.ID,
		ServiceInstallationID:    installation.ID,
		ServiceMemberID:          member.ID,
		ServiceKind:              KindServiceAccount,
		TargetKind:               KindServiceAccount,
		TargetRef:                ServiceAccountTargetRef(serviceAccountID),
		DeliveryScopeID:          serviceAccountScopeID,
		PermissionCheckScopeID:   serviceAccountScopeID,
		AuthoritySubjectMemberID: fixture.HumanMemberID,
		DeliveryContract:         []byte(`{"mode":"service_account"}`),
		CreatedByMemberID:        fixture.HumanMemberID,
		CreatedByActorType:       "human",
		CreatedByActorID:         fixture.UserID,
		CreatedVia:               "test",
		ActiveBindingKey:         "binding-integrity-service-account",
		Now:                      now,
	}
	err = upsertTargetBindingInTransaction(ctx, db, store, params)
	require.ErrorIs(t, err, ErrConflict)

	params.AuthoritySubjectMemberID = ""
	err = upsertTargetBindingInTransaction(ctx, db, store, params)
	require.ErrorIs(t, err, ErrConflict)

	params.AuthoritySubjectMemberID = fixture.ServiceMemberID
	legacyIDTarget := params
	legacyIDTarget.TargetRef = serviceAccountID
	err = upsertTargetBindingInTransaction(ctx, db, store, legacyIDTarget)
	require.ErrorIs(t, err, ErrConflict)

	require.NoError(t, upsertTargetBindingInTransaction(ctx, db, store, params))
	requireTargetBindingCount(t, ctx, db, 1, params.ActiveBindingKey)
	staleReplay := params
	staleReplay.DeliveryContract = []byte(`{"mode":"other"}`)
	err = upsertTargetBindingInTransaction(ctx, db, store, staleReplay)
	require.ErrorIs(t, err, ErrConflict)
}

func TestTargetBindingRestoreRequiresActiveSubstrates(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	db := startServiceActorRollbackDB(t, ctx)
	now := time.Date(2026, 7, 10, 4, 15, 0, 0, time.UTC)
	store := New(db)
	tests := []struct {
		name   string
		mutate func(testing.TB, targetBindingIntegritySeed, serviceActorFixture)
	}{
		{
			name: "definition inactive",
			mutate: func(t testing.TB, seed targetBindingIntegritySeed, _ serviceActorFixture) {
				mustExec(t, db, `UPDATE service_definitions SET status = 'inactive' WHERE id = $1`, seed.definition.ID)
			},
		},
		{
			name: "installation disabled",
			mutate: func(t testing.TB, seed targetBindingIntegritySeed, _ serviceActorFixture) {
				mustExec(t, db, `UPDATE service_installations SET status = 'disabled', disabled_at = $2 WHERE id = $1`, seed.installation.ID, now)
			},
		},
		{
			name: "service member inactive",
			mutate: func(t testing.TB, seed targetBindingIntegritySeed, _ serviceActorFixture) {
				mustExec(t, db, `UPDATE service_members SET status = 'inactive' WHERE id = $1`, seed.member.ID)
			},
		},
		{
			name: "base service member inactive",
			mutate: func(t testing.TB, _ targetBindingIntegritySeed, fixture serviceActorFixture) {
				mustExec(t, db, `UPDATE members SET status = 'inactive' WHERE id = $1`, fixture.ServiceMemberID)
			},
		},
		{
			name: "scope inactive",
			mutate: func(t testing.TB, _ targetBindingIntegritySeed, fixture serviceActorFixture) {
				mustExec(t, db, `UPDATE scopes SET status = 'inactive' WHERE id = $1`, fixture.ScopeID)
			},
		},
		{
			name: "scope archived",
			mutate: func(t testing.TB, _ targetBindingIntegritySeed, fixture serviceActorFixture) {
				mustExec(t, db, `UPDATE scopes SET archived_at = $2 WHERE id = $1`, fixture.ScopeID, now)
			},
		},
	}
	for index, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			suffix := fmt.Sprintf("restore_active_%d", index)
			fixture := seedServiceActorFixture(t, db, suffix, now)
			seed := seedTargetBindingIntegrityApp(t, ctx, store, fixture, suffix, now)
			require.NoError(t, upsertTargetBindingInTransaction(ctx, db, store, seed.params))
			require.NoError(t, coredb.WithTx(ctx, db, func(tx coredb.Tx) error {
				_, _, err := store.InTx(tx).DisableTargetBinding(ctx, seed.params.WorkspaceID, seed.params.TargetKind, seed.params.TargetRef, seed.params.ServiceInstallationID, "test", now)
				return err
			}))
			test.mutate(t, seed, fixture)

			err := upsertTargetBindingInTransaction(ctx, db, store, seed.params)
			require.ErrorIs(t, err, ErrConflict)
			var status string
			var activeKey sql.NullString
			require.NoError(t, db.QueryRow(ctx, `SELECT status, active_binding_key FROM service_target_bindings WHERE id = $1`, seed.params.ID).Scan(&status, &activeKey))
			require.Equal(t, StatusDisabled, status)
			require.False(t, activeKey.Valid)
		})
	}
}

func TestTargetBindingImmutableReplayComparisonCoversEveryField(t *testing.T) {
	identity := targetBindingImmutableIdentity{
		WorkspaceID:              "wsp",
		ServiceDefinitionID:      "svcdef",
		ServiceInstallationID:    "svcinst",
		ServiceMemberID:          "svcmem",
		ServiceKind:              KindServiceAccount,
		TargetKind:               "channel",
		TargetRef:                "chn",
		DeliveryScopeID:          "scp_delivery",
		PermissionCheckScopeID:   "scp_permission",
		AuthoritySubjectMemberID: "mem_authority",
		DeliveryContract:         []byte(`{"mode":"app","version":1}`),
		ActiveBindingKey:         "binding-key",
	}
	binding := TargetBinding{
		WorkspaceID:              identity.WorkspaceID,
		ServiceDefinitionID:      identity.ServiceDefinitionID,
		ServiceInstallationID:    identity.ServiceInstallationID,
		ServiceMemberID:          identity.ServiceMemberID,
		ServiceKind:              identity.ServiceKind,
		TargetKind:               identity.TargetKind,
		TargetRef:                identity.TargetRef,
		DeliveryScopeID:          identity.DeliveryScopeID,
		PermissionCheckScopeID:   identity.PermissionCheckScopeID,
		AuthoritySubjectMemberID: identity.AuthoritySubjectMemberID,
		DeliveryContract:         identity.DeliveryContract,
		ActiveBindingKey:         identity.ActiveBindingKey,
	}
	require.True(t, activeTargetBindingReplayMatches(binding, identity))

	tests := []struct {
		name   string
		mutate func(*targetBindingImmutableIdentity)
	}{
		{"workspace", func(value *targetBindingImmutableIdentity) { value.WorkspaceID += "_other" }},
		{"definition", func(value *targetBindingImmutableIdentity) { value.ServiceDefinitionID += "_other" }},
		{"installation", func(value *targetBindingImmutableIdentity) { value.ServiceInstallationID += "_other" }},
		{"member", func(value *targetBindingImmutableIdentity) { value.ServiceMemberID += "_other" }},
		{"kind", func(value *targetBindingImmutableIdentity) { value.ServiceKind += "_other" }},
		{"target kind", func(value *targetBindingImmutableIdentity) { value.TargetKind = "dm" }},
		{"target ref", func(value *targetBindingImmutableIdentity) { value.TargetRef += "_other" }},
		{"delivery scope", func(value *targetBindingImmutableIdentity) { value.DeliveryScopeID += "_other" }},
		{"permission scope", func(value *targetBindingImmutableIdentity) { value.PermissionCheckScopeID += "_other" }},
		{"authority", func(value *targetBindingImmutableIdentity) { value.AuthoritySubjectMemberID += "_other" }},
		{"delivery contract", func(value *targetBindingImmutableIdentity) {
			value.DeliveryContract = []byte(`{"mode":"other","version":1}`)
		}},
		{"active key", func(value *targetBindingImmutableIdentity) { value.ActiveBindingKey += "_other" }},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			candidate := identity
			test.mutate(&candidate)
			require.False(t, activeTargetBindingReplayMatches(binding, candidate))
		})
	}
}

func seedTargetBindingIntegrityApp(t testing.TB, ctx context.Context, store *Store, fixture serviceActorFixture, suffix string, now time.Time) targetBindingIntegritySeed {
	t.Helper()
	definition, err := store.UpsertDefinition(ctx, DefinitionUpsert{
		ID:          "svc_binding_integrity_" + suffix,
		ServiceKind: KindApp,
		Slug:        "binding-integrity-" + suffix,
		Name:        "Binding Integrity " + suffix,
		Status:      StatusActive,
		Now:         now,
	})
	require.NoError(t, err)
	installation, err := store.UpsertInstallation(ctx, InstallationUpsert{
		ID:                  "svcinst_binding_integrity_" + suffix,
		WorkspaceID:         fixture.WorkspaceID,
		ServiceDefinitionID: definition.ID,
		ServiceKind:         definition.ServiceKind,
		Status:              StatusActive,
		CreatedByMemberID:   fixture.HumanMemberID,
		Now:                 now,
	})
	require.NoError(t, err)
	member, err := store.UpsertMember(ctx, MemberUpsert{
		ID:                    "svcmem_binding_integrity_" + suffix,
		WorkspaceID:           fixture.WorkspaceID,
		ServiceDefinitionID:   definition.ID,
		ServiceInstallationID: installation.ID,
		MemberID:              fixture.ServiceMemberID,
		ServiceKind:           definition.ServiceKind,
		Handle:                "binding-integrity-" + suffix,
		DisplayName:           "Binding Integrity " + suffix,
		Status:                StatusActive,
		Now:                   now,
	})
	require.NoError(t, err)
	return targetBindingIntegritySeed{
		definition:   definition,
		installation: installation,
		member:       member,
		params: TargetBindingUpsert{
			ID:                       "svctb_binding_integrity_" + suffix,
			WorkspaceID:              fixture.WorkspaceID,
			ServiceDefinitionID:      definition.ID,
			ServiceInstallationID:    installation.ID,
			ServiceMemberID:          member.ID,
			ServiceKind:              definition.ServiceKind,
			TargetKind:               "channel",
			TargetRef:                fixture.ScopeID,
			DeliveryScopeID:          fixture.ScopeID,
			PermissionCheckScopeID:   fixture.ScopeID,
			AuthoritySubjectMemberID: fixture.HumanMemberID,
			DeliveryContract:         []byte(`{"mode":"app","version":1}`),
			CreatedByMemberID:        fixture.HumanMemberID,
			CreatedByActorType:       "human",
			CreatedByActorID:         fixture.UserID,
			CreatedVia:               "test",
			ScopeSnapshot:            []byte(`{"source":"test"}`),
			Metadata:                 []byte(`{"source":"test"}`),
			Now:                      now,
			ActiveBindingKey:         "binding-integrity-" + suffix,
		},
	}
}

func upsertTargetBindingInTransaction(ctx context.Context, db coredb.DB, store *Store, params TargetBindingUpsert) error {
	return coredb.WithTx(ctx, db, func(tx coredb.Tx) error {
		_, err := store.InTx(tx).UpsertTargetBinding(ctx, params)
		return err
	})
}

func requireTargetBindingCount(t testing.TB, ctx context.Context, db coredb.QueryRower, want int, activeBindingKey string) {
	t.Helper()
	var count int
	require.NoError(t, db.QueryRow(ctx, `
SELECT COUNT(*)
FROM service_target_bindings
WHERE active_binding_key = $1
`, activeBindingKey).Scan(&count))
	require.Equal(t, want, count)
}
