package api

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/pocket-id/pocket-id/backend/internal/common"
	"github.com/pocket-id/pocket-id/backend/internal/model"
	"github.com/pocket-id/pocket-id/backend/internal/oidc"
	testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing"
)

func TestAPICrudAndPermissionDiff(t *testing.T) {
	db := testutils.NewDatabaseForTest(t)
	svc := New(Dependencies{DB: db}).service

	created, err := svc.Create(t.Context(), apiCreateDto{Name: "Orders API", Resource: "https://api.orders.example.com"})
	require.NoError(t, err)
	assert.NotEmpty(t, created.ID)

	// The resource is unique.
	_, err = svc.Create(t.Context(), apiCreateDto{Name: "Dup", Resource: "https://api.orders.example.com"})
	require.ErrorIs(t, err, &common.AlreadyInUseError{})

	desc := "Read orders"
	updated, err := svc.UpdatePermissions(t.Context(), created.ID, apiPermissionsUpdateDto{Permissions: []apiPermissionInputDto{
		{Key: "read:orders", Name: "Read orders", Description: &desc},
		{Key: "write:orders", Name: "Write orders"},
	}})
	require.NoError(t, err)
	assert.Len(t, updated.Permissions, 2)

	// Grant a client the read:orders permission for both subject types, then remove that permission
	// and confirm the grants are cleaned up while write:orders (and its key) survives.
	readPerm := findPermission(updated, "read:orders")
	require.NotNil(t, readPerm)
	require.NoError(t, db.Create(&model.OidcClient{Base: model.Base{ID: "client-1"}, Name: "Client 1"}).Error)
	require.NoError(t, db.Create(&OidcClientAllowedAPIPermission{OidcClientID: "client-1", APIPermissionID: readPerm.ID, SubjectType: oidc.SubjectTypeUser}).Error)
	require.NoError(t, db.Create(&OidcClientAllowedAPIPermission{OidcClientID: "client-1", APIPermissionID: readPerm.ID, SubjectType: oidc.SubjectTypeClient}).Error)

	updated, err = svc.UpdatePermissions(t.Context(), created.ID, apiPermissionsUpdateDto{Permissions: []apiPermissionInputDto{
		{Key: "write:orders", Name: "Write orders (renamed)"},
	}})
	require.NoError(t, err)
	require.Len(t, updated.Permissions, 1)
	assert.Equal(t, "write:orders", updated.Permissions[0].Key)
	assert.Equal(t, "Write orders (renamed)", updated.Permissions[0].Name)

	var grantCount int64
	require.NoError(t, db.Model(&OidcClientAllowedAPIPermission{}).Where("api_permission_id = ?", readPerm.ID).Count(&grantCount).Error)
	assert.Equal(t, int64(0), grantCount)

	renamed, err := svc.Update(t.Context(), created.ID, apiUpdateDto{Name: "Orders"})
	require.NoError(t, err)
	assert.Equal(t, "Orders", renamed.Name)
	require.NotNil(t, renamed.UpdatedAt)

	require.NoError(t, svc.Delete(t.Context(), created.ID))
	_, err = svc.Get(t.Context(), nil, created.ID)
	require.Error(t, err)
}

func TestClientApiAccessAllowList(t *testing.T) {
	db := testutils.NewDatabaseForTest(t)
	svc := New(Dependencies{DB: db}).service

	require.NoError(t, db.Create(&model.OidcClient{Base: model.Base{ID: "client-1"}, Name: "Client 1"}).Error)

	orders, err := svc.Create(t.Context(), apiCreateDto{Name: "Orders", Resource: "https://api.orders.example.com"})
	require.NoError(t, err)
	orders, err = svc.UpdatePermissions(t.Context(), orders.ID, apiPermissionsUpdateDto{Permissions: []apiPermissionInputDto{
		{Key: "read:orders", Name: "Read"},
		{Key: "write:orders", Name: "Write"},
	}})
	require.NoError(t, err)
	readID := findPermission(orders, "read:orders").ID
	writeID := findPermission(orders, "write:orders").ID

	// Unknown IDs are filtered out, and the subject types are stored independently.
	applied, err := svc.SetClientAPIAccess(t.Context(), "client-1", ClientAPIAccess{
		UserDelegatedPermissionIDs: []string{readID, "does-not-exist"},
		ClientPermissionIDs:        []string{writeID, "does-not-exist"},
	})
	require.NoError(t, err)
	assert.ElementsMatch(t, []string{readID}, applied.UserDelegatedPermissionIDs)
	assert.ElementsMatch(t, []string{writeID}, applied.ClientPermissionIDs)

	got, err := svc.GetClientAPIAccess(t.Context(), "client-1")
	require.NoError(t, err)
	assert.ElementsMatch(t, []string{readID}, got.UserDelegatedPermissionIDs)
	assert.ElementsMatch(t, []string{writeID}, got.ClientPermissionIDs)

	// The same permission can be granted for both subject types, and both sets are fully replaced on each call.
	_, err = svc.SetClientAPIAccess(t.Context(), "client-1", ClientAPIAccess{
		UserDelegatedPermissionIDs: []string{readID, writeID},
		ClientPermissionIDs:        []string{readID},
	})
	require.NoError(t, err)
	got, err = svc.GetClientAPIAccess(t.Context(), "client-1")
	require.NoError(t, err)
	assert.ElementsMatch(t, []string{readID, writeID}, got.UserDelegatedPermissionIDs)
	assert.ElementsMatch(t, []string{readID}, got.ClientPermissionIDs)

	// Clearing one subject type leaves the other untouched.
	_, err = svc.SetClientAPIAccess(t.Context(), "client-1", ClientAPIAccess{ClientPermissionIDs: []string{readID}})
	require.NoError(t, err)
	got, err = svc.GetClientAPIAccess(t.Context(), "client-1")
	require.NoError(t, err)
	assert.Empty(t, got.UserDelegatedPermissionIDs)
	assert.ElementsMatch(t, []string{readID}, got.ClientPermissionIDs)

	// Clearing everything.
	_, err = svc.SetClientAPIAccess(t.Context(), "client-1", ClientAPIAccess{})
	require.NoError(t, err)
	got, err = svc.GetClientAPIAccess(t.Context(), "client-1")
	require.NoError(t, err)
	assert.Empty(t, got.UserDelegatedPermissionIDs)
	assert.Empty(t, got.ClientPermissionIDs)

	// An unknown client is rejected (surfaces as 404 at the HTTP layer).
	_, err = svc.SetClientAPIAccess(t.Context(), "nope", ClientAPIAccess{UserDelegatedPermissionIDs: []string{readID}})
	require.Error(t, err)
}

// TestAllowedScopesForAudienceFiltersBySubjectType guards that the scopes resolved for a flow
// only come from the grants of that flow's subject type.
func TestAllowedScopesForAudienceFiltersBySubjectType(t *testing.T) {
	db := testutils.NewDatabaseForTest(t)
	svc := New(Dependencies{DB: db}).service

	require.NoError(t, db.Create(&model.OidcClient{Base: model.Base{ID: "client-1"}, Name: "Client 1"}).Error)

	orders, err := svc.Create(t.Context(), apiCreateDto{Name: "Orders", Resource: "https://api.orders.example.com"})
	require.NoError(t, err)
	orders, err = svc.UpdatePermissions(t.Context(), orders.ID, apiPermissionsUpdateDto{Permissions: []apiPermissionInputDto{
		{Key: "read:orders", Name: "Read"},
		{Key: "write:orders", Name: "Write"},
	}})
	require.NoError(t, err)
	readID := findPermission(orders, "read:orders").ID
	writeID := findPermission(orders, "write:orders").ID

	_, err = svc.SetClientAPIAccess(t.Context(), "client-1", ClientAPIAccess{
		UserDelegatedPermissionIDs: []string{readID},
		ClientPermissionIDs:        []string{writeID},
	})
	require.NoError(t, err)

	userScopes, exists, err := svc.AllowedScopesForAudience(t.Context(), nil, "client-1", "https://api.orders.example.com", oidc.SubjectTypeUser)
	require.NoError(t, err)
	require.True(t, exists)
	assert.ElementsMatch(t, []string{"read:orders"}, userScopes)

	clientScopes, exists, err := svc.AllowedScopesForAudience(t.Context(), nil, "client-1", "https://api.orders.example.com", oidc.SubjectTypeClient)
	require.NoError(t, err)
	require.True(t, exists)
	assert.ElementsMatch(t, []string{"write:orders"}, clientScopes)

	// The fosite widening still sees the union of both subject types.
	scopes, audiences, err := svc.ClientAPIScopesAndAudiences(t.Context(), nil, "client-1")
	require.NoError(t, err)
	assert.ElementsMatch(t, []string{"read:orders", "write:orders"}, scopes)
	assert.ElementsMatch(t, []string{"https://api.orders.example.com"}, audiences)
}

func TestUpdatePermissionsRejectsReservedKeys(t *testing.T) {
	db := testutils.NewDatabaseForTest(t)
	svc := New(Dependencies{DB: db}).service

	orders, err := svc.Create(t.Context(), apiCreateDto{Name: "Orders", Resource: "https://api.orders.example.com"})
	require.NoError(t, err)

	for _, key := range []string{"openid", "profile", "email", "email_verified", "groups", "offline_access", "Email"} {
		_, err := svc.UpdatePermissions(t.Context(), orders.ID, apiPermissionsUpdateDto{Permissions: []apiPermissionInputDto{
			{Key: key, Name: "Reserved"},
		}})
		require.Error(t, err, "key %q must be rejected", key)
		var validationErr *common.ValidationError
		require.ErrorAs(t, err, &validationErr)
	}
}

func TestUpdatePermissionsRejectsDuplicateKeys(t *testing.T) {
	db := testutils.NewDatabaseForTest(t)
	svc := New(Dependencies{DB: db}).service

	orders, err := svc.Create(t.Context(), apiCreateDto{Name: "Orders", Resource: "https://api.orders.example.com"})
	require.NoError(t, err)

	// Two rows with the same key must be rejected rather than silently coalesced last-wins
	_, err = svc.UpdatePermissions(t.Context(), orders.ID, apiPermissionsUpdateDto{Permissions: []apiPermissionInputDto{
		{Key: "read:orders", Name: "Read"},
		{Key: "read:orders", Name: "Read again"},
	}})
	require.Error(t, err)
	var validationErr *common.ValidationError
	require.ErrorAs(t, err, &validationErr)
}

func TestUpdatePermissionsRejectsInvalidKeyCharacters(t *testing.T) {
	db := testutils.NewDatabaseForTest(t)
	svc := New(Dependencies{DB: db}).service

	orders, err := svc.Create(t.Context(), apiCreateDto{Name: "Orders", Resource: "https://api.orders.example.com"})
	require.NoError(t, err)

	// A space corrupts the space-delimited scope claim, and the unit separator is the consent delimiter
	for _, key := range []string{"read orders", "read\x1forders", "read\"orders", "bad\\key", "tab\tkey"} {
		_, err := svc.UpdatePermissions(t.Context(), orders.ID, apiPermissionsUpdateDto{Permissions: []apiPermissionInputDto{
			{Key: key, Name: "Invalid"},
		}})
		require.Error(t, err, "key %q must be rejected", key)
		var validationErr *common.ValidationError
		require.ErrorAs(t, err, &validationErr)
	}

	// A valid scope-token key is accepted
	_, err = svc.UpdatePermissions(t.Context(), orders.ID, apiPermissionsUpdateDto{Permissions: []apiPermissionInputDto{
		{Key: "read:orders", Name: "Read"},
	}})
	require.NoError(t, err)
}

func TestCreateRejectsIssuerResource(t *testing.T) {
	db := testutils.NewDatabaseForTest(t)
	const issuer = "https://id.example.com"
	svc := New(Dependencies{DB: db, Issuer: issuer}).service

	// The issuer itself, a trailing-slash variant, and a different-cased variant are all reserved
	for _, resource := range []string{issuer, issuer + "/", "https://ID.example.com"} {
		_, err := svc.Create(t.Context(), apiCreateDto{Name: "Reserved", Resource: resource})
		require.Error(t, err, "resource %q must be rejected", resource)
		var validationErr *common.ValidationError
		require.ErrorAs(t, err, &validationErr)
	}

	// A normal resource is accepted
	_, err := svc.Create(t.Context(), apiCreateDto{Name: "Orders", Resource: "https://api.orders.example.com"})
	require.NoError(t, err)
}

func TestCreateAcceptsAbsoluteResourceURIs(t *testing.T) {
	db := testutils.NewDatabaseForTest(t)
	svc := New(Dependencies{DB: db}).service

	for _, resource := range []string{"https://api.orders.example.com", "api://PocketID", "urn:my-app"} {
		_, err := svc.Create(t.Context(), apiCreateDto{Name: "Orders", Resource: resource})
		require.NoError(t, err, "resource %q must be accepted", resource)
	}
}

func TestDescribePermissions(t *testing.T) {
	db := testutils.NewDatabaseForTest(t)
	svc := New(Dependencies{DB: db}).service

	orders, err := svc.Create(t.Context(), apiCreateDto{Name: "Orders", Resource: "https://api.orders.example.com"})
	require.NoError(t, err)
	desc := "Read orders"
	_, err = svc.UpdatePermissions(t.Context(), orders.ID, apiPermissionsUpdateDto{Permissions: []apiPermissionInputDto{
		{Key: "read:orders", Name: "Read orders", Description: &desc},
		{Key: "write:orders", Name: "Write orders"},
	}})
	require.NoError(t, err)

	infos, err := svc.DescribePermissions(t.Context(), "https://api.orders.example.com", []string{"read:orders", "unknown"})
	require.NoError(t, err)
	require.Len(t, infos, 1)
	assert.Equal(t, "read:orders", infos[0].Key)
	assert.Equal(t, "Read orders", infos[0].Name)
	require.NotNil(t, infos[0].Description)
	assert.Equal(t, "Read orders", *infos[0].Description)
}

func findPermission(api API, key string) *Permission {
	for i := range api.Permissions {
		if api.Permissions[i].Key == key {
			return &api.Permissions[i]
		}
	}
	return nil
}
