package repository

import (
	"context"
	"crypto/sha256"
	"fmt"
	"testing"

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

	coremodel "security-center-backend/internal/model"
)

func TestAPIKeyCreateAndFindByHash(t *testing.T) {
	ctx := context.Background()
	db := newAPIKeyRepositoryTestDB(t)
	repo := NewAPIKeyRepository(db)

	hash := fmt.Sprintf("%x", sha256.Sum256([]byte("secret-key-1")))
	key := &coremodel.APIKey{
		KeyPrefix:       "sc_abcdefghijklmno",
		KeyHash:         hash,
		AgentName:       "asset-agent",
		OwnerUserID:     42,
		PermissionScope: coremodel.StringArray{"caasm:view_assets"},
		Status:          coremodel.APIKeyStatusActive,
	}
	require.NoError(t, repo.Create(ctx, key))
	require.NotZero(t, key.ID)

	found, err := repo.FindByHash(ctx, hash)
	require.NoError(t, err)
	require.NotNil(t, found)
	require.Equal(t, key.ID, found.ID)
	require.Equal(t, "asset-agent", found.AgentName)
	require.Equal(t, uint64(42), found.OwnerUserID)
	require.Equal(t, coremodel.APIKeyStatusActive, found.Status)
	require.Equal(t, coremodel.StringArray{"caasm:view_assets"}, found.PermissionScope)

	missing, err := repo.FindByHash(ctx, fmt.Sprintf("%x", sha256.Sum256([]byte("no-such-key"))))
	require.NoError(t, err)
	require.Nil(t, missing)
}

func TestAPIKeyTouchLastUsed(t *testing.T) {
	ctx := context.Background()
	db := newAPIKeyRepositoryTestDB(t)
	repo := NewAPIKeyRepository(db)

	hash := fmt.Sprintf("%x", sha256.Sum256([]byte("secret-key-2")))
	key := &coremodel.APIKey{
		KeyPrefix:       "sc_touchtest12345",
		KeyHash:         hash,
		AgentName:       "touch-agent",
		OwnerUserID:     7,
		PermissionScope: coremodel.StringArray{},
		Status:          coremodel.APIKeyStatusActive,
	}
	require.NoError(t, repo.Create(ctx, key))

	found, err := repo.FindByHash(ctx, hash)
	require.NoError(t, err)
	require.NotNil(t, found)
	require.Nil(t, found.LastUsedAt)

	require.NoError(t, repo.TouchLastUsed(ctx, key.ID))

	found, err = repo.FindByHash(ctx, hash)
	require.NoError(t, err)
	require.NotNil(t, found)
	require.NotNil(t, found.LastUsedAt)
}

// apiKeyTestUser mirrors the minimal users table columns needed for the
// api_keys.owner_user_id foreign key, avoiding model.User's MySQL-only enum.
type apiKeyTestUser struct {
	coremodel.Base
	Name  string `gorm:"size:255"`
	Email string `gorm:"size:320"`
}

func (apiKeyTestUser) TableName() string {
	return "users"
}

func newAPIKeyRepositoryTestDB(t *testing.T) *gorm.DB {
	t.Helper()

	dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
	db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&apiKeyTestUser{}, &coremodel.APIKey{}))

	return db
}

// createAPIKeyForOwner persists one API key owned by ownerUserID; name makes the
// hash/prefix unique per key.
func createAPIKeyForOwner(t *testing.T, repo APIKeyRepository, ownerUserID uint64, name string, status coremodel.APIKeyStatus) *coremodel.APIKey {
	t.Helper()
	key := &coremodel.APIKey{
		KeyPrefix:       "sc_" + name,
		KeyHash:         fmt.Sprintf("%x", sha256.Sum256([]byte(name))),
		AgentName:       name,
		OwnerUserID:     ownerUserID,
		PermissionScope: coremodel.StringArray{},
		Status:          status,
	}
	require.NoError(t, repo.Create(context.Background(), key))
	require.NotZero(t, key.ID)
	return key
}

func TestAPIKeyFindByIDForOwnerOwnerIsolation(t *testing.T) {
	ctx := context.Background()
	repo := NewAPIKeyRepository(newAPIKeyRepositoryTestDB(t))

	keyA := createAPIKeyForOwner(t, repo, 100, "agent-a-1", coremodel.APIKeyStatusActive)
	keyB := createAPIKeyForOwner(t, repo, 200, "agent-b-1", coremodel.APIKeyStatusActive)

	// Owner A querying its own key hits.
	found, err := repo.FindByIDForOwner(ctx, keyA.ID, 100)
	require.NoError(t, err)
	require.NotNil(t, found)
	require.Equal(t, keyA.ID, found.ID)

	// Owner B querying A's key misses (404 semantics), and vice versa.
	missing, err := repo.FindByIDForOwner(ctx, keyA.ID, 200)
	require.NoError(t, err)
	require.Nil(t, missing)

	missing, err = repo.FindByIDForOwner(ctx, keyB.ID, 100)
	require.NoError(t, err)
	require.Nil(t, missing)

	// Non-existent id misses too.
	missing, err = repo.FindByIDForOwner(ctx, 999999, 100)
	require.NoError(t, err)
	require.Nil(t, missing)
}

func TestAPIKeyListByOwner(t *testing.T) {
	ctx := context.Background()
	repo := NewAPIKeyRepository(newAPIKeyRepositoryTestDB(t))

	keyA1 := createAPIKeyForOwner(t, repo, 100, "agent-a-1", coremodel.APIKeyStatusActive)
	keyA2 := createAPIKeyForOwner(t, repo, 100, "agent-a-2", coremodel.APIKeyStatusActive)
	createAPIKeyForOwner(t, repo, 200, "agent-b-1", coremodel.APIKeyStatusActive)

	keys, err := repo.ListByOwner(ctx, 100)
	require.NoError(t, err)
	require.Len(t, keys, 2)
	// Ordered by id DESC: newest first.
	require.Equal(t, keyA2.ID, keys[0].ID)
	require.Equal(t, keyA1.ID, keys[1].ID)

	keys, err = repo.ListByOwner(ctx, 200)
	require.NoError(t, err)
	require.Len(t, keys, 1)
	require.Equal(t, uint64(200), keys[0].OwnerUserID)

	keys, err = repo.ListByOwner(ctx, 300)
	require.NoError(t, err)
	require.Empty(t, keys)
}

func TestAPIKeyRevokeOwnerIsolation(t *testing.T) {
	ctx := context.Background()
	repo := NewAPIKeyRepository(newAPIKeyRepositoryTestDB(t))

	keyA := createAPIKeyForOwner(t, repo, 100, "agent-a-1", coremodel.APIKeyStatusActive)
	keyB := createAPIKeyForOwner(t, repo, 200, "agent-b-1", coremodel.APIKeyStatusActive)

	// Revoking someone else's key fails and leaves the record untouched.
	err := repo.Revoke(ctx, keyB.ID, 100)
	require.ErrorIs(t, err, ErrAPIKeyNotFoundOrNotOwned)

	stillActive, err := repo.FindByID(ctx, keyB.ID)
	require.NoError(t, err)
	require.NotNil(t, stillActive)
	require.Equal(t, coremodel.APIKeyStatusActive, stillActive.Status)

	// Revoking a non-existent id also fails.
	err = repo.Revoke(ctx, 999999, 100)
	require.ErrorIs(t, err, ErrAPIKeyNotFoundOrNotOwned)

	// Revoking own key succeeds and flips the status.
	require.NoError(t, repo.Revoke(ctx, keyA.ID, 100))

	revoked, err := repo.FindByID(ctx, keyA.ID)
	require.NoError(t, err)
	require.NotNil(t, revoked)
	require.Equal(t, coremodel.APIKeyStatusRevoked, revoked.Status)
}

func TestAPIKeyAdminFallbackFindByIDAndListAll(t *testing.T) {
	ctx := context.Background()
	repo := NewAPIKeyRepository(newAPIKeyRepositoryTestDB(t))

	keyA := createAPIKeyForOwner(t, repo, 100, "agent-a-1", coremodel.APIKeyStatusActive)
	createAPIKeyForOwner(t, repo, 200, "agent-b-1", coremodel.APIKeyStatusActive)

	// FindByID has no owner scoping: admin can read another user's key.
	found, err := repo.FindByID(ctx, keyA.ID)
	require.NoError(t, err)
	require.NotNil(t, found)
	require.Equal(t, uint64(100), found.OwnerUserID)

	missing, err := repo.FindByID(ctx, 999999)
	require.NoError(t, err)
	require.Nil(t, missing)

	// ListAll without filters returns every owner's keys.
	all, err := repo.ListAll(ctx, ListAPIKeysFilter{})
	require.NoError(t, err)
	require.Len(t, all, 2)
	owners := map[uint64]bool{all[0].OwnerUserID: true, all[1].OwnerUserID: true}
	require.True(t, owners[100])
	require.True(t, owners[200])
}

func TestAPIKeyListAllFilterAndPagination(t *testing.T) {
	ctx := context.Background()
	repo := NewAPIKeyRepository(newAPIKeyRepositoryTestDB(t))

	createAPIKeyForOwner(t, repo, 100, "agent-1", coremodel.APIKeyStatusActive)
	createAPIKeyForOwner(t, repo, 100, "agent-2", coremodel.APIKeyStatusRevoked)
	createAPIKeyForOwner(t, repo, 200, "agent-3", coremodel.APIKeyStatusActive)
	createAPIKeyForOwner(t, repo, 200, "agent-4", coremodel.APIKeyStatusActive)
	createAPIKeyForOwner(t, repo, 300, "agent-5", coremodel.APIKeyStatusRevoked)

	// Status filter.
	active, err := repo.ListAll(ctx, ListAPIKeysFilter{Status: string(coremodel.APIKeyStatusActive)})
	require.NoError(t, err)
	require.Len(t, active, 3)
	for _, k := range active {
		require.Equal(t, coremodel.APIKeyStatusActive, k.Status)
	}

	// OwnerUserID filter.
	owner100 := uint64(100)
	owned, err := repo.ListAll(ctx, ListAPIKeysFilter{OwnerUserID: &owner100})
	require.NoError(t, err)
	require.Len(t, owned, 2)
	for _, k := range owned {
		require.Equal(t, uint64(100), k.OwnerUserID)
	}

	// Combined filters.
	combined, err := repo.ListAll(ctx, ListAPIKeysFilter{
		OwnerUserID: &owner100,
		Status:      string(coremodel.APIKeyStatusRevoked),
	})
	require.NoError(t, err)
	require.Len(t, combined, 1)
	require.Equal(t, "agent-2", combined[0].AgentName)

	// Pagination: 5 keys total, page size 2 -> pages of 2/2/1, id DESC.
	page1, err := repo.ListAll(ctx, ListAPIKeysFilter{Page: 1, PageSize: 2})
	require.NoError(t, err)
	require.Len(t, page1, 2)
	require.Equal(t, "agent-5", page1[0].AgentName)
	require.Equal(t, "agent-4", page1[1].AgentName)

	page2, err := repo.ListAll(ctx, ListAPIKeysFilter{Page: 2, PageSize: 2})
	require.NoError(t, err)
	require.Len(t, page2, 2)
	require.Equal(t, "agent-3", page2[0].AgentName)
	require.Equal(t, "agent-2", page2[1].AgentName)

	page3, err := repo.ListAll(ctx, ListAPIKeysFilter{Page: 3, PageSize: 2})
	require.NoError(t, err)
	require.Len(t, page3, 1)
	require.Equal(t, "agent-1", page3[0].AgentName)

	page4, err := repo.ListAll(ctx, ListAPIKeysFilter{Page: 4, PageSize: 2})
	require.NoError(t, err)
	require.Empty(t, page4)
}
