package repotokens

import (
	"context"
	"testing"
	"time"

	"github.com/glebarez/sqlite"
	"github.com/stretchr/testify/require"
	"gorm.io/gorm"

	store "github.com/tidbcloud/AgentTeam/internal/store/repotokens"
)

func testService(t *testing.T, now time.Time) (*Service, *gorm.DB) {
	t.Helper()
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&store.Token{}, &store.Event{}, &store.AgentSuppression{}))
	return NewService(store.New(db), func() time.Time { return now }), db
}

func validHumanRequest() CreateHumanRequest {
	return CreateHumanRequest{
		WorkspaceID:      "wsp_1",
		ActorPrincipalID: "usr_1",
		DisplayName:      "Docs",
		ProviderHost:     "github.com",
		RepositoryScope:  RepositoryScope{Mode: "repositories", Repositories: []string{"pingcap/agentteam"}},
		Capabilities:     []string{"repo:read", "repo:clone"},
	}
}

func TestServiceCreatesAndResolvesMeshXOwnedCredentialWithoutPersistingPlaintext(t *testing.T) {
	now := time.Date(2026, 7, 16, 4, 0, 0, 0, time.UTC)
	svc, db := testService(t, now)

	created, err := svc.CreateHuman(context.Background(), validHumanRequest())
	require.NoError(t, err)
	require.Contains(t, created.Plaintext, "ags_rat_")
	require.Equal(t, TokenTypeHuman, created.Token.Type)
	require.Equal(t, "usr_1", created.Token.OwnerPrincipalID)

	var row store.Token
	require.NoError(t, db.First(&row, "public_id = ?", created.Token.ID).Error)
	require.NotEmpty(t, row.VerifierHash)
	require.NotContains(t, row.VerifierHash, created.Plaintext)
	require.NotContains(t, row.RepositoryScopeJSON, created.Plaintext)
	var events []store.Event
	require.NoError(t, db.Where("token_id = ?", row.ID).Order("id ASC").Find(&events).Error)
	require.Len(t, events, 2)
	require.Equal(t, []string{EventCreated, EventPlaintextRevealed}, []string{events[0].Type, events[1].Type})
	require.NotContains(t, events[1].MetadataJSON, created.Plaintext)

	resolved, failure, err := svc.ValidateCredential(context.Background(), created.Plaintext)
	require.NoError(t, err)
	require.Empty(t, failure)
	require.Equal(t, created.Token.ID, resolved.ID)
}

func TestServiceReconcileAgentRecordsScopeAndCapabilityChanges(t *testing.T) {
	now := time.Date(2026, 7, 16, 4, 0, 0, 0, time.UTC)
	svc, db := testService(t, now)
	request := ReconcileAgentRequest{
		WorkspaceID: "wsp_1", ActorPrincipalID: "system", AgentID: "agt_1",
		ProviderHost:    "github.com",
		RepositoryScope: RepositoryScope{Mode: "repositories", Repositories: []string{"pingcap/agentteam"}},
		Capabilities:    []string{"repo:read"}, Reason: AgentTokenReasonAgentCreated,
	}
	token, err := svc.ReconcileAgent(context.Background(), request)
	require.NoError(t, err)

	request.RepositoryScope = RepositoryScope{Mode: "repositories", Repositories: []string{"pingcap/other"}}
	request.Capabilities = []string{"repo:read", "repo:clone"}
	request.Reason = AgentTokenReasonAgentUpdated
	_, err = svc.ReconcileAgent(context.Background(), request)
	require.NoError(t, err)

	var row store.Token
	require.NoError(t, db.First(&row, "public_id = ?", token.ID).Error)
	var events []store.Event
	require.NoError(t, db.Where("token_id = ?", row.ID).Order("id ASC").Find(&events).Error)
	require.Len(t, events, 3)
	require.Equal(t, []string{EventCreated, EventScopeUpdated, EventCapabilitiesUpdated},
		[]string{events[0].Type, events[1].Type, events[2].Type})
	require.Contains(t, events[1].MetadataJSON, AgentTokenReasonAgentUpdated)
}

func TestServiceSyncLeaseDefaultsToBoundedExpiry(t *testing.T) {
	now := time.Date(2026, 7, 16, 4, 0, 0, 0, time.UTC)
	svc, _ := testService(t, now)

	created, err := svc.CreateSyncLease(context.Background(), CreateSyncLeaseRequest{
		WorkspaceID:      "wsp_1",
		ActorPrincipalID: "daemon_1",
		DisplayName:      "Skill sync",
		ProviderHost:     "github.com",
		RepositoryScope:  RepositoryScope{Mode: "repositories", Repositories: []string{"pingcap/skill"}},
		Capabilities:     []string{"repo:read", "repo:clone"},
	})
	require.NoError(t, err)
	require.Equal(t, TokenTypeSyncLease, created.Token.Type)
	require.Equal(t, now.Add(30*time.Minute), *created.Token.ExpiresAt)
}

func TestServiceHumanAPIOnlyListsCurrentPrincipalTokens(t *testing.T) {
	now := time.Date(2026, 7, 16, 4, 0, 0, 0, time.UTC)
	svc, _ := testService(t, now)
	first, err := svc.CreateHuman(context.Background(), validHumanRequest())
	require.NoError(t, err)
	other := validHumanRequest()
	other.ActorPrincipalID = "usr_other"
	_, err = svc.CreateHuman(context.Background(), other)
	require.NoError(t, err)

	result, err := svc.List(context.Background(), ListRequest{WorkspaceID: "wsp_1", ActorPrincipalID: "usr_1"})
	require.NoError(t, err)
	require.Len(t, result.Items, 1)
	require.Equal(t, first.Token.ID, result.Items[0].ID)

	_, err = svc.Get(context.Background(), GetRequest{WorkspaceID: "wsp_1", ActorPrincipalID: "usr_other", ID: first.Token.ID})
	require.ErrorIs(t, err, ErrTokenNotFound)
}

func TestServiceRevokesCredentialAndRecordsEvent(t *testing.T) {
	now := time.Date(2026, 7, 16, 4, 0, 0, 0, time.UTC)
	svc, _ := testService(t, now)
	created, err := svc.CreateHuman(context.Background(), validHumanRequest())
	require.NoError(t, err)

	revoked, err := svc.Revoke(context.Background(), RevokeRequest{
		WorkspaceID:      "wsp_1",
		ActorPrincipalID: "usr_1",
		ID:               created.Token.ID,
		Reason:           "user_requested",
	})
	require.NoError(t, err)
	require.Equal(t, StatusRevoked, revoked.Status)

	_, failure, err := svc.ValidateCredential(context.Background(), created.Plaintext)
	require.NoError(t, err)
	require.Equal(t, ValidationFailureRevokedToken, failure)

	events, err := svc.Events(context.Background(), EventListRequest{WorkspaceID: "wsp_1", ActorPrincipalID: "usr_1", ID: created.Token.ID})
	require.NoError(t, err)
	require.Equal(t, EventRevoked, events.Items[0].Type)
}

func TestServiceForceRevokedAgentCannotBeRecreatedAcrossInstances(t *testing.T) {
	now := time.Date(2026, 7, 16, 4, 0, 0, 0, time.UTC)
	first, db := testService(t, now)
	state := DesiredStateFromAgentRepositoryAccess("wsp_1", "system", "agt_1", "", RepositoryScope{
		Mode: "repositories", Repositories: []string{"pingcap/agentteam"},
	}, AgentPermissionWorkspaceWrite)
	created, err := first.ReconcileAgentDesiredState(context.Background(), state, AgentTokenReasonAgentCreated)
	require.NoError(t, err)
	require.Equal(t, TokenTypeAgent, created.Type)

	_, err = first.RevokeAgent(context.Background(), RevokeAgentRequest{
		WorkspaceID: "wsp_1", ActorPrincipalID: "security", AgentID: "agt_1",
		Reason: AgentTokenReasonSecurityResponse, Force: true,
	})
	require.NoError(t, err)

	second := NewService(store.New(db), func() time.Time { return now.Add(time.Minute) })
	_, err = second.ReconcileAgentDesiredState(context.Background(), state, AgentTokenReasonAgentUpdated)
	require.ErrorIs(t, err, ErrTokenStateConflict)
}

func TestServiceAuthorizesRepositoryScopeAndCapability(t *testing.T) {
	now := time.Date(2026, 7, 16, 4, 0, 0, 0, time.UTC)
	svc, _ := testService(t, now)
	created, err := svc.CreateHuman(context.Background(), validHumanRequest())
	require.NoError(t, err)
	token, failure, err := svc.ValidateCredential(context.Background(), created.Plaintext)
	require.NoError(t, err)
	require.Empty(t, failure)

	require.NoError(t, svc.AuthorizeRepository(token, "PINGCAP/AGENTTEAM.git", "repo:clone"))
	require.ErrorIs(t, svc.AuthorizeRepository(token, "pingcap/other", "repo:clone"), ErrPermissionDenied)
	require.ErrorIs(t, svc.AuthorizeRepository(token, "pingcap/agentteam", "repo:write"), ErrPermissionDenied)
}

func TestDesiredStateFromAgentRepositoryAccessRequiresExplicitPermissionMode(t *testing.T) {
	scope := RepositoryScope{Mode: "repositories", Repositories: []string{"pingcap/agentteam"}}
	workspaceWrite := DesiredStateFromAgentRepositoryAccess("wsp_1", "system", "agt_1", "", scope, AgentPermissionWorkspaceWrite)
	require.True(t, workspaceWrite.Enabled)
	require.Equal(t, []string{AgentCapabilityRepositoryRead, AgentCapabilityRepositoryClone}, workspaceWrite.Capabilities)
	fullAccess := DesiredStateFromAgentRepositoryAccess("wsp_1", "system", "agt_1", "", scope, AgentPermissionFullAccess)
	require.Equal(t, []string{AgentCapabilityRepositoryRead, AgentCapabilityRepositoryClone, AgentCapabilityRepositoryWrite}, fullAccess.Capabilities)
	for _, mode := range []string{"", "unknown", "read_only"} {
		require.False(t, DesiredStateFromAgentRepositoryAccess("wsp_1", "system", "agt_1", "", scope, mode).Enabled)
	}
}
