package oidc

import (
	"context"
	"slices"
	"testing"

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

	"github.com/pocket-id/pocket-id/backend/internal/dto"
)

// fakeAPIAccess implements APIAccessProvider from an audience -> subject type -> allowed-scopes map.
// An audience present in the map exists as an API; a subject type present under it grants access, together with the scopes that subject may request, which can be none.
type fakeAPIAccess struct {
	allowed map[string]map[SubjectType][]string
}

// userAccess builds a fakeAPIAccess with only user-delegated grants, for tests that don't care about the split.
func userAccess(allowed map[string][]string) fakeAPIAccess {
	f := fakeAPIAccess{allowed: map[string]map[SubjectType][]string{}}
	for audience, scopes := range allowed {
		f.allowed[audience] = map[SubjectType][]string{SubjectTypeUser: scopes}
	}
	return f
}

func (f fakeAPIAccess) ClientAPIScopes(_ context.Context, _ *gorm.DB, _ string, _ bool) ([]string, []string, error) {
	seen := map[string]struct{}{}
	var scopes, audiences []string
	for audience, bySubject := range f.allowed {
		audiences = append(audiences, audience)
		for _, scopeKeys := range bySubject {
			for _, key := range scopeKeys {
				if _, ok := seen[key]; !ok {
					seen[key] = struct{}{}
					scopes = append(scopes, key)
				}
			}
		}
	}
	return scopes, audiences, nil
}

func (f fakeAPIAccess) AllowedScopesForAudience(_ context.Context, _ *gorm.DB, _ string, audience string, subjectType SubjectType) ([]string, bool, bool, error) {
	bySubject, exists := f.allowed[audience]
	if !exists {
		return nil, false, false, nil
	}
	scopes, hasAccess := bySubject[subjectType]
	return scopes, true, hasAccess, nil
}

func (f fakeAPIAccess) DescribePermissions(_ context.Context, audience string, keys []string) ([]dto.ScopeInfoDto, error) {
	bySubject, ok := f.allowed[audience]
	if !ok {
		return nil, nil
	}
	var infos []dto.ScopeInfoDto
	for _, key := range keys {
		for _, allowed := range bySubject {
			if slices.Contains(allowed, key) {
				infos = append(infos, dto.ScopeInfoDto{Key: key, Name: key})
				break
			}
		}
	}
	return infos, nil
}

func TestResolveResourceDefaultIsLoginToken(t *testing.T) {
	// With no resource the token is a plain login token audienced to the requesting client
	audience, granted, err := resolveResource(t.Context(), nil, nil, "client-1", "", []string{"openid", "profile"}, SubjectTypeUser)
	require.NoError(t, err)
	assert.Equal(t, "client-1", audience)
	assert.Equal(t, []string{"openid", "profile"}, granted)
}

func TestResolveResourceRejectsCustomScopeWithoutResource(t *testing.T) {
	// Requesting a custom scope without targeting its API must be rejected, not dropped.
	_, _, err := resolveResource(t.Context(), nil, nil, "client-1", "", []string{"openid", "read:orders"}, SubjectTypeUser)
	require.Error(t, err)
}

func TestResolveResourceCustomAPIGrantsValidScopes(t *testing.T) {
	provider := userAccess(map[string][]string{
		"https://api.orders.example.com": {"read:orders", "write:orders"},
	})

	audience, granted, err := resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com", []string{"openid", "read:orders"}, SubjectTypeUser)
	require.NoError(t, err)
	assert.Equal(t, "https://api.orders.example.com", audience)
	// openid stays (identity, for the ID token); read:orders is allowed for this API.
	assert.ElementsMatch(t, []string{"openid", "read:orders"}, granted)
}

func TestResolveResourceTrimsTrailingSlashes(t *testing.T) {
	provider := userAccess(map[string][]string{
		"https://api.orders.example.com": {"read:orders"},
	})

	audience, granted, err := resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com///", []string{"read:orders"}, SubjectTypeUser)
	require.NoError(t, err)
	assert.Equal(t, "https://api.orders.example.com", audience)
	assert.Equal(t, []string{"read:orders"}, granted)
}

func TestResolveResourceRejectsScopeFromAnotherAPI(t *testing.T) {
	provider := userAccess(map[string][]string{
		"https://api.orders.example.com": {"read:orders", "write:orders"},
	})
	// write:billing belongs to a different API than the one targeted -> rejected.
	_, _, err := resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com", []string{"openid", "write:billing"}, SubjectTypeUser)
	require.Error(t, err)
}

func TestResolveResourceUnknownIsRejected(t *testing.T) {
	provider := userAccess(map[string][]string{})
	_, _, err := resolveResource(t.Context(), nil, provider, "client-1", "https://api.unknown.example.com", []string{"read"}, SubjectTypeUser)
	require.Error(t, err)
}

func TestResolveResourceUnauthorizedClientIsRejected(t *testing.T) {
	// The API exists but the client was not granted access to it for any subject type.
	provider := fakeAPIAccess{allowed: map[string]map[SubjectType][]string{
		"https://api.orders.example.com": {},
	}}
	_, _, err := resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com", []string{"read:orders"}, SubjectTypeUser)
	require.Error(t, err)
}

// TestResolveResourceAccessWithoutPermissions covers a client that may reach an API without holding a single
// permission, which is what the MCP specification expects: the token is audienced to the resource and carries no scope.
func TestResolveResourceAccessWithoutPermissions(t *testing.T) {
	provider := userAccess(map[string][]string{
		"https://api.orders.example.com": {},
	})

	audience, granted, err := resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com", nil, SubjectTypeUser)
	require.NoError(t, err)
	assert.Equal(t, "https://api.orders.example.com", audience)
	assert.Empty(t, granted)

	// Identity scopes still ride along, so the same request can also produce an ID token
	_, granted, err = resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com", []string{"openid"}, SubjectTypeUser)
	require.NoError(t, err)
	assert.Equal(t, []string{"openid"}, granted)

	// Access alone does not make a custom scope requestable
	_, _, err = resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com", []string{"read:orders"}, SubjectTypeUser)
	require.Error(t, err)
}

// TestResolveResourceSubjectTypesAreIndependent guards the core of the user/client separation:
// a permission granted for one subject type must not leak into the other, so a scope users may
// delegate cannot be minted machine-to-machine and a machine scope cannot ride along on a login.
func TestResolveResourceSubjectTypesAreIndependent(t *testing.T) {
	provider := fakeAPIAccess{allowed: map[string]map[SubjectType][]string{
		"https://api.orders.example.com": {
			SubjectTypeUser:   {"read:orders"},
			SubjectTypeClient: {"write:orders"},
		},
	}}

	// The user-delegated grant works for user flows...
	audience, granted, err := resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com", []string{"read:orders"}, SubjectTypeUser)
	require.NoError(t, err)
	assert.Equal(t, "https://api.orders.example.com", audience)
	assert.ElementsMatch(t, []string{"read:orders"}, granted)

	// ...but not for the client itself.
	_, _, err = resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com", []string{"read:orders"}, SubjectTypeClient)
	require.Error(t, err)

	// The client grant works machine-to-machine...
	audience, granted, err = resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com", []string{"write:orders"}, SubjectTypeClient)
	require.NoError(t, err)
	assert.Equal(t, "https://api.orders.example.com", audience)
	assert.ElementsMatch(t, []string{"write:orders"}, granted)

	// ...but users cannot be asked to delegate it.
	_, _, err = resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com", []string{"write:orders"}, SubjectTypeUser)
	require.Error(t, err)
}

// TestResolveResourceClientWithoutClientGrantsIsDenied models a client that only has user-delegated
// grants: the API must stay unreachable through the client credentials grant entirely.
func TestResolveResourceClientWithoutClientGrantsIsDenied(t *testing.T) {
	provider := fakeAPIAccess{allowed: map[string]map[SubjectType][]string{
		"https://api.orders.example.com": {
			SubjectTypeUser: {"read:orders"},
		},
	}}

	_, _, err := resolveResource(t.Context(), nil, provider, "client-1", "https://api.orders.example.com", nil, SubjectTypeClient)
	require.Error(t, err)
}

func TestConsentScopeKeyQualifiesCustomScopesOnly(t *testing.T) {
	// Identity scopes stay bare for backward compatibility with existing consents.
	assert.Equal(t, "profile", consentScopeKey("https://api.orders.example.com", "profile"))
	// Custom scopes are qualified by audience so the same key on two APIs is distinct.
	assert.Equal(t, "https://api.orders.example.com\x1fread", consentScopeKey("https://api.orders.example.com", "read"))
	assert.NotEqual(t,
		consentScopeKey("https://api.orders.example.com", "read"),
		consentScopeKey("https://api.billing.example.com", "read"),
	)
}
