package test

import (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"

	"github.com/kms/api-key-service/internal/auth"
)

func TestPermissionHierarchy_InitializeDefaultPermissions(t *testing.T) {
	hierarchy := auth.NewPermissionHierarchy()

	// Test that default permissions are created
	permissions := hierarchy.ListPermissions()
	assert.NotEmpty(t, permissions)

	// Test specific permissions exist
	permissionNames := make(map[string]bool)
	for _, perm := range permissions {
		permissionNames[perm.Name] = true
	}

	expectedPermissions := []string{
		"admin", "read", "write",
		"app.admin", "app.read", "app.write", "app.create", "app.update", "app.delete",
		"token.admin", "token.read", "token.write", "token.create", "token.revoke", "token.verify",
		"permission.admin", "permission.read", "permission.write", "permission.grant", "permission.revoke",
		"user.admin", "user.read", "user.write",
	}

	for _, expected := range expectedPermissions {
		assert.True(t, permissionNames[expected], "Permission %s should exist", expected)
	}
}

func TestPermissionHierarchy_InitializeDefaultRoles(t *testing.T) {
	hierarchy := auth.NewPermissionHierarchy()

	// Test that default roles are created
	roles := hierarchy.ListRoles()
	assert.NotEmpty(t, roles)

	// Test specific roles exist
	roleNames := make(map[string]bool)
	for _, role := range roles {
		roleNames[role.Name] = true
	}

	expectedRoles := []string{
		"super_admin", "app_admin", "developer", "viewer", "token_manager",
	}

	for _, expected := range expectedRoles {
		assert.True(t, roleNames[expected], "Role %s should exist", expected)
	}
}

func TestPermissionManager_HasPermission(t *testing.T) {
	configMock := NewTestConfig()
	configMock.values["CACHE_ENABLED"] = "false" // Disable cache for testing

	logger := zap.NewNop()
	pm := auth.NewPermissionManager(configMock, logger)

	tests := []struct {
		name           string
		userID         string
		appID          string
		permission     string
		expectedResult bool
		description    string
	}{
		{
			name:           "admin user has admin permission",
			userID:         "admin@example.com",
			appID:          "test-app",
			permission:     "admin",
			expectedResult: true,
			description:    "Admin users should have admin permissions",
		},
		{
			name:           "developer user has token.create permission",
			userID:         "dev@example.com",
			appID:          "test-app",
			permission:     "token.create",
			expectedResult: true,
			description:    "Developer users should have token creation permissions",
		},
		{
			name:           "viewer user has read permission",
			userID:         "viewer@example.com",
			appID:          "test-app",
			permission:     "app.read",
			expectedResult: true,
			description:    "Viewer users should have read permissions",
		},
		{
			name:           "viewer user denied write permission",
			userID:         "viewer@example.com",
			appID:          "test-app",
			permission:     "app.write",
			expectedResult: false,
			description:    "Viewer users should not have write permissions",
		},
		{
			name:           "non-existent permission",
			userID:         "admin@example.com",
			appID:          "test-app",
			permission:     "non.existent",
			expectedResult: false,
			description:    "Non-existent permissions should be denied",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			evaluation, err := pm.HasPermission(ctx, tt.userID, tt.appID, tt.permission)

			require.NoError(t, err)
			assert.NotNil(t, evaluation)
			assert.Equal(t, tt.expectedResult, evaluation.Granted, tt.description)
			assert.Equal(t, tt.permission, evaluation.Permission)
			assert.NotZero(t, evaluation.EvaluatedAt)

			if evaluation.Granted {
				assert.NotEmpty(t, evaluation.GrantedBy, "Granted permissions should have GrantedBy information")
			} else {
				assert.NotEmpty(t, evaluation.DeniedReason, "Denied permissions should have a reason")
			}
		})
	}
}

func TestPermissionManager_EvaluateBulkPermissions(t *testing.T) {
	configMock := NewTestConfig()
	configMock.values["CACHE_ENABLED"] = "false"

	logger := zap.NewNop()
	pm := auth.NewPermissionManager(configMock, logger)

	ctx := context.Background()
	req := &auth.BulkPermissionRequest{
		UserID: "dev@example.com",
		AppID:  "test-app",
		Permissions: []string{
			"app.read",
			"token.create",
			"token.read",
			"app.delete", // Should be denied for developer
			"admin",      // Should be denied for developer
		},
	}

	response, err := pm.EvaluateBulkPermissions(ctx, req)

	require.NoError(t, err)
	assert.NotNil(t, response)
	assert.Equal(t, req.UserID, response.UserID)
	assert.Equal(t, req.AppID, response.AppID)
	assert.Len(t, response.Results, len(req.Permissions))

	// Check specific results
	assert.True(t, response.Results["app.read"].Granted, "Developer should have app.read permission")
	assert.True(t, response.Results["token.create"].Granted, "Developer should have token.create permission")
	assert.True(t, response.Results["token.read"].Granted, "Developer should have token.read permission")
	assert.False(t, response.Results["app.delete"].Granted, "Developer should not have app.delete permission")
	assert.False(t, response.Results["admin"].Granted, "Developer should not have admin permission")
}

func TestPermissionManager_AddPermission(t *testing.T) {
	configMock := NewTestConfig()

	logger := zap.NewNop()
	pm := auth.NewPermissionManager(configMock, logger)

	tests := []struct {
		name        string
		permission  *auth.Permission
		expectError bool
		description string
	}{
		{
			name: "add valid permission",
			permission: &auth.Permission{
				Name:        "custom.permission",
				Description: "Custom permission for testing",
				Parent:      "read",
				Level:       2,
				Resource:    "custom",
				Action:      "test",
			},
			expectError: false,
			description: "Valid permissions should be added successfully",
		},
		{
			name: "add permission without name",
			permission: &auth.Permission{
				Description: "Permission without name",
				Parent:      "read",
				Level:       2,
			},
			expectError: true,
			description: "Permissions without names should be rejected",
		},
		{
			name: "add permission with non-existent parent",
			permission: &auth.Permission{
				Name:        "invalid.permission",
				Description: "Permission with invalid parent",
				Parent:      "non.existent",
				Level:       2,
			},
			expectError: true,
			description: "Permissions with non-existent parents should be rejected",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := pm.AddPermission(tt.permission)

			if tt.expectError {
				assert.Error(t, err, tt.description)
			} else {
				assert.NoError(t, err, tt.description)
				
				// Verify permission was added
				permissions := pm.ListPermissions()
				found := false
				for _, perm := range permissions {
					if perm.Name == tt.permission.Name {
						found = true
						assert.Equal(t, tt.permission.Description, perm.Description)
						assert.Equal(t, tt.permission.Parent, perm.Parent)
						break
					}
				}
				assert.True(t, found, "Added permission should be found in the list")
			}
		})
	}
}

func TestPermissionManager_AddRole(t *testing.T) {
	configMock := NewTestConfig()

	logger := zap.NewNop()
	pm := auth.NewPermissionManager(configMock, logger)

	tests := []struct {
		name        string
		role        *auth.Role
		expectError bool
		description string
	}{
		{
			name: "add valid role",
			role: &auth.Role{
				Name:        "custom_role",
				Description: "Custom role for testing",
				Permissions: []string{"read", "app.read"},
				Metadata:    map[string]string{"level": "custom"},
			},
			expectError: false,
			description: "Valid roles should be added successfully",
		},
		{
			name: "add role without name",
			role: &auth.Role{
				Description: "Role without name",
				Permissions: []string{"read"},
			},
			expectError: true,
			description: "Roles without names should be rejected",
		},
		{
			name: "add role with non-existent permission",
			role: &auth.Role{
				Name:        "invalid_role",
				Description: "Role with invalid permission",
				Permissions: []string{"non.existent.permission"},
			},
			expectError: true,
			description: "Roles with non-existent permissions should be rejected",
		},
		{
			name: "add role with non-existent inherited role",
			role: &auth.Role{
				Name:        "invalid_inherited_role",
				Description: "Role with invalid inheritance",
				Permissions: []string{"read"},
				Inherits:    []string{"non_existent_role"},
			},
			expectError: true,
			description: "Roles with non-existent inherited roles should be rejected",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := pm.AddRole(tt.role)

			if tt.expectError {
				assert.Error(t, err, tt.description)
			} else {
				assert.NoError(t, err, tt.description)
				
				// Verify role was added
				roles := pm.ListRoles()
				found := false
				for _, role := range roles {
					if role.Name == tt.role.Name {
						found = true
						assert.Equal(t, tt.role.Description, role.Description)
						assert.Equal(t, tt.role.Permissions, role.Permissions)
						break
					}
				}
				assert.True(t, found, "Added role should be found in the list")
			}
		})
	}
}

func TestPermissionManager_ListPermissions(t *testing.T) {
	configMock := NewTestConfig()

	logger := zap.NewNop()
	pm := auth.NewPermissionManager(configMock, logger)

	permissions := pm.ListPermissions()

	// Should have default permissions
	assert.NotEmpty(t, permissions)

	// Should be sorted by level and name
	for i := 1; i < len(permissions); i++ {
		prev := permissions[i-1]
		curr := permissions[i]
		
		if prev.Level == curr.Level {
			assert.True(t, prev.Name <= curr.Name, "Permissions at same level should be sorted by name")
		} else {
			assert.True(t, prev.Level <= curr.Level, "Permissions should be sorted by level")
		}
	}

	// Verify hierarchy structure
	for _, perm := range permissions {
		if perm.Parent != "" {
			// Find parent permission
			parentFound := false
			for _, parent := range permissions {
				if parent.Name == perm.Parent {
					parentFound = true
					assert.True(t, parent.Level < perm.Level, "Parent should have lower level than child")
					assert.Contains(t, parent.Children, perm.Name, "Parent should contain child in children list")
					break
				}
			}
			assert.True(t, parentFound, "Parent permission should exist for %s", perm.Name)
		}
	}
}

func TestPermissionManager_ListRoles(t *testing.T) {
	configMock := NewTestConfig()

	logger := zap.NewNop()
	pm := auth.NewPermissionManager(configMock, logger)

	roles := pm.ListRoles()

	// Should have default roles
	assert.NotEmpty(t, roles)

	// Should be sorted by name
	for i := 1; i < len(roles); i++ {
		assert.True(t, roles[i-1].Name <= roles[i].Name, "Roles should be sorted by name")
	}

	// Verify all permissions in roles exist
	allPermissions := pm.ListPermissions()
	permissionNames := make(map[string]bool)
	for _, perm := range allPermissions {
		permissionNames[perm.Name] = true
	}

	for _, role := range roles {
		for _, perm := range role.Permissions {
			assert.True(t, permissionNames[perm], "Role %s references non-existent permission %s", role.Name, perm)
		}
	}
}

func TestPermissionManager_InvalidatePermissionCache(t *testing.T) {
	configMock := NewTestConfig()

	logger := zap.NewNop()
	pm := auth.NewPermissionManager(configMock, logger)

	ctx := context.Background()
	err := pm.InvalidatePermissionCache(ctx, "user123", "app123")

	// Should not error (currently just logs)
	assert.NoError(t, err)
}

func TestPermissionHierarchy_BuildHierarchy(t *testing.T) {
	hierarchy := auth.NewPermissionHierarchy()

	// Test that parent-child relationships are built correctly
	permissions := hierarchy.ListPermissions()
	
	// Find admin permission
	var adminPerm *auth.Permission
	for _, perm := range permissions {
		if perm.Name == "admin" {
			adminPerm = perm
			break
		}
	}
	
	require.NotNil(t, adminPerm, "Admin permission should exist")
	
	// Admin should have children
	assert.NotEmpty(t, adminPerm.Children, "Admin permission should have children")
	
	// Check that app.admin is a child of admin
	assert.Contains(t, adminPerm.Children, "app.admin", "app.admin should be a child of admin")
	
	// Find app.write permission
	var appWritePerm *auth.Permission
	for _, perm := range permissions {
		if perm.Name == "app.write" {
			appWritePerm = perm
			break
		}
	}
	
	require.NotNil(t, appWritePerm, "app.write permission should exist")
	
	// app.write should have children
	assert.NotEmpty(t, appWritePerm.Children, "app.write permission should have children")
	assert.Contains(t, appWritePerm.Children, "app.create", "app.create should be a child of app.write")
	assert.Contains(t, appWritePerm.Children, "app.update", "app.update should be a child of app.write")
	assert.Contains(t, appWritePerm.Children, "app.delete", "app.delete should be a child of app.write")
}

// Benchmark tests for permission operations
func BenchmarkPermissionManager_HasPermission(b *testing.B) {
	configMock := NewTestConfig()
	configMock.values["CACHE_ENABLED"] = "false"

	logger := zap.NewNop()
	pm := auth.NewPermissionManager(configMock, logger)
	ctx := context.Background()

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, err := pm.HasPermission(ctx, "dev@example.com", "test-app", "token.create")
		if err != nil {
			b.Fatal(err)
		}
	}
}

func BenchmarkPermissionManager_EvaluateBulkPermissions(b *testing.B) {
	configMock := NewTestConfig()
	configMock.values["CACHE_ENABLED"] = "false"

	logger := zap.NewNop()
	pm := auth.NewPermissionManager(configMock, logger)
	ctx := context.Background()

	req := &auth.BulkPermissionRequest{
		UserID: "dev@example.com",
		AppID:  "test-app",
		Permissions: []string{
			"app.read", "token.create", "token.read", "app.delete", "admin",
		},
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, err := pm.EvaluateBulkPermissions(ctx, req)
		if err != nil {
			b.Fatal(err)
		}
	}
}

func BenchmarkPermissionManager_ListPermissions(b *testing.B) {
	configMock := NewTestConfig()

	logger := zap.NewNop()
	pm := auth.NewPermissionManager(configMock, logger)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		permissions := pm.ListPermissions()
		if len(permissions) == 0 {
			b.Fatal("No permissions returned")
		}
	}
}

func BenchmarkPermissionManager_ListRoles(b *testing.B) {
	configMock := NewTestConfig()

	logger := zap.NewNop()
	pm := auth.NewPermissionManager(configMock, logger)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		roles := pm.ListRoles()
		if len(roles) == 0 {
			b.Fatal("No roles returned")
		}
	}
}

// Test permission hierarchy traversal
func TestPermissionHierarchy_PermissionInheritance(t *testing.T) {
	configMock := NewTestConfig()
	configMock.values["CACHE_ENABLED"] = "false"

	logger := zap.NewNop()
	pm := auth.NewPermissionManager(configMock, logger)

	// Test that admin users get hierarchical permissions
	ctx := context.Background()
	
	// Admin should have all permissions through hierarchy
	adminPermissions := []string{
		"admin",
		"app.admin",
		"token.admin",
		"permission.admin",
		"user.admin",
	}

	for _, perm := range adminPermissions {
		evaluation, err := pm.HasPermission(ctx, "admin@example.com", "test-app", perm)
		require.NoError(t, err)
		assert.True(t, evaluation.Granted, "Admin should have %s permission", perm)
	}
}

// Test role inheritance
func TestPermissionManager_RoleInheritance(t *testing.T) {
	configMock := NewTestConfig()

	logger := zap.NewNop()
	pm := auth.NewPermissionManager(configMock, logger)

	// Add a role that inherits from another role
	parentRole := &auth.Role{
		Name:        "base_role",
		Description: "Base role with basic permissions",
		Permissions: []string{"read", "app.read"},
		Metadata:    map[string]string{"level": "base"},
	}

	childRole := &auth.Role{
		Name:        "extended_role",
		Description: "Extended role that inherits from base",
		Permissions: []string{"write"},
		Inherits:    []string{"base_role"},
		Metadata:    map[string]string{"level": "extended"},
	}

	err := pm.AddRole(parentRole)
	require.NoError(t, err)

	err = pm.AddRole(childRole)
	require.NoError(t, err)

	// Verify roles were added
	roles := pm.ListRoles()
	roleNames := make(map[string]*auth.Role)
	for _, role := range roles {
		roleNames[role.Name] = role
	}

	assert.Contains(t, roleNames, "base_role")
	assert.Contains(t, roleNames, "extended_role")
	assert.Equal(t, []string{"base_role"}, roleNames["extended_role"].Inherits)
}
