package analytics_test

import (
	"context"
	"fmt"
	"testing"

	"github.com/nexuscrm/backend/internal/features/analytics"
	"github.com/nexuscrm/backend/internal/features/permissions"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

// MockPermissionChecker implements services.PermissionChecker
type MockPermissionChecker struct {
	mock.Mock
}

func (m *MockPermissionChecker) CheckObjectPermissionWithUser(ctx context.Context, objectName string, permission string, user *models.UserSession) bool {
	args := m.Called(ctx, objectName, permission, user)
	return args.Bool(0)
}

func (m *MockPermissionChecker) CheckFieldVisibilityWithUser(ctx context.Context, objectName string, fieldName string, user *models.UserSession) bool {
	args := m.Called(ctx, objectName, fieldName, user)
	return args.Bool(0)
}

func (m *MockPermissionChecker) GetSubordinateUserIDs(ctx context.Context, roleID string) ([]string, error) {
	args := m.Called(ctx, roleID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]string), args.Error(1)
}

func (m *MockPermissionChecker) BuildRLSResult(ctx context.Context, schema *models.ObjectMetadata, objectName string, user *models.UserSession) *permissions.RLSResult {
	args := m.Called(ctx, schema, objectName, user)
	if args.Get(0) == nil {
		return nil
	}
	return args.Get(0).(*permissions.RLSResult)
}

// MockMetadataProvider implements services.MetadataProvider
type MockMetadataProvider struct {
	mock.Mock
}

func (m *MockMetadataProvider) GetSchema(ctx context.Context, objectName string) *models.ObjectMetadata {
	args := m.Called(ctx, objectName)
	if args.Get(0) == nil {
		return nil
	}
	return args.Get(0).(*models.ObjectMetadata)
}

func TestSecurityValidator_ValidateAndRewrite(t *testing.T) {
	// Setup user
	stdUser := &models.UserSession{
		ID:        "user-123",
		ProfileID: constants.ProfileStandardUser,
	}

	adminUser := &models.UserSession{
		ID:        "admin-123",
		ProfileID: constants.ProfileSystemAdmin,
	}

	// Schema with OwnerID
	accountSchema := &models.ObjectMetadata{
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldName, Type: constants.FieldTypeString},
			{APIName: constants.FieldOwnerID, Type: constants.FieldTypeReference},
			{APIName: "Industry", Type: constants.FieldTypePicklist},
		},
	}

	// Schema without OwnerID
	configSchema := &models.ObjectMetadata{
		APIName: "SystemConfig",
		Fields: []models.FieldMetadata{
			{APIName: "ConfigKey", Type: constants.FieldTypeString},
			{APIName: "Value", Type: constants.FieldTypeString},
		},
	}

	t.Run("Permissions: Block Table Access", func(t *testing.T) {
		mockPerms := new(MockPermissionChecker)
		mockMeta := new(MockMetadataProvider)

		// Expect permission check failure
		// Note: AST traversal visits Fields before From clause, so Field check happens first!
		mockPerms.On("CheckFieldVisibilityWithUser", mock.Anything, "Account", constants.FieldName, stdUser).Return(true)
		mockPerms.On("CheckObjectPermissionWithUser", mock.Anything, "Account", constants.PermRead, stdUser).Return(false)

		validator := analytics.NewSecurityValidator(mockPerms, mockMeta)

		sql := fmt.Sprintf("SELECT %s FROM Account", constants.FieldName)
		_, _, err := validator.ValidateAndRewrite(context.Background(), sql, nil, stdUser, nil)

		assert.Error(t, err)
		assert.Contains(t, err.Error(), "cannot read table 'Account'")

		mockPerms.AssertExpectations(t)
	})

	t.Run("Permissions: Block Field Access", func(t *testing.T) {
		mockPerms := new(MockPermissionChecker)
		mockMeta := new(MockMetadataProvider)

		// Table OK, Field "salary" blocked
		mockPerms.On("CheckObjectPermissionWithUser", mock.Anything, "Employee", constants.PermRead, stdUser).Return(true)
		mockPerms.On("CheckFieldVisibilityWithUser", mock.Anything, "Employee", "Salary", stdUser).Return(false)
		mockPerms.On("CheckFieldVisibilityWithUser", mock.Anything, "Employee", constants.FieldName, stdUser).Return(true) // name OK

		validator := analytics.NewSecurityValidator(mockPerms, mockMeta)

		sql := fmt.Sprintf("SELECT %s, Salary FROM Employee", constants.FieldName)
		_, _, err := validator.ValidateAndRewrite(context.Background(), sql, nil, stdUser, nil)

		assert.Error(t, err)
		assert.Contains(t, err.Error(), "cannot read field 'Salary'")
	})

	t.Run("RLS: Standard User - Inject Owner Filter", func(t *testing.T) {
		mockPerms := new(MockPermissionChecker)
		mockMeta := new(MockMetadataProvider)

		mockPerms.On("CheckObjectPermissionWithUser", mock.Anything, "Account", constants.PermRead, stdUser).Return(true)
		mockPerms.On("CheckObjectPermissionWithUser", mock.Anything, "Account", "ViewAllRecords", stdUser).Return(false)
		mockPerms.On("CheckFieldVisibilityWithUser", mock.Anything, "Account", constants.FieldName, stdUser).Return(true)
		mockPerms.On("CheckFieldVisibilityWithUser", mock.Anything, "Account", "Industry", stdUser).Return(true)
		mockPerms.On("BuildRLSResult", mock.Anything, mock.Anything, "Account", stdUser).Return(&permissions.RLSResult{
			ObjectAPIName: "Account",
			HasOwnerField: true,
			UserID:        stdUser.ID,
			OwnerUserIDs:  []string{stdUser.ID},
		})

		// Return schema with owner_id
		mockMeta.On("GetSchema", mock.Anything, "Account").Return(accountSchema)

		validator := analytics.NewSecurityValidator(mockPerms, mockMeta)

		sql := fmt.Sprintf("SELECT %s FROM Account WHERE Industry = 'Tech'", constants.FieldName)
		rewritten, _, err := validator.ValidateAndRewrite(context.Background(), sql, nil, stdUser, nil)

		assert.NoError(t, err)
		// rewritten SQL should contain owner_id check
		// Note: The AST restoration puts backticks usually
		assert.Contains(t, rewritten, constants.FieldOwnerID)
		assert.Contains(t, rewritten, "user-123")
		// TiDB parser output format check: `owner_id` or just owner_id?
		// We can check stricter logic if needed.
		// Expected: SELECT `name` FROM `Account` WHERE `Industry` = 'Tech' AND `owner_id` = 'user-123'
	})

	t.Run("RLS: Admin User - No injection", func(t *testing.T) {
		mockPerms := new(MockPermissionChecker)
		mockMeta := new(MockMetadataProvider)

		mockPerms.On("CheckObjectPermissionWithUser", mock.Anything, "Account", constants.PermRead, adminUser).Return(true)
		mockPerms.On("CheckFieldVisibilityWithUser", mock.Anything, "Account", constants.FieldName, adminUser).Return(true)
		// BuildRLSResult returns Skip=true for admin (SuperUser)
		mockPerms.On("BuildRLSResult", mock.Anything, mock.Anything, "Account", adminUser).Return(&permissions.RLSResult{Skip: true})
		mockMeta.On("GetSchema", mock.Anything, "Account").Return(accountSchema)

		validator := analytics.NewSecurityValidator(mockPerms, mockMeta)

		sql := fmt.Sprintf("SELECT %s FROM Account", constants.FieldName)
		rewritten, _, err := validator.ValidateAndRewrite(context.Background(), sql, nil, adminUser, nil)

		assert.NoError(t, err)
		assert.NotContains(t, rewritten, "owner_id")
		assert.NotContains(t, rewritten, "user-123")
	})

	t.Run("RLS: Object without Owner - No injection", func(t *testing.T) {
		mockPerms := new(MockPermissionChecker)
		mockMeta := new(MockMetadataProvider)

		mockPerms.On("CheckObjectPermissionWithUser", mock.Anything, "SystemConfig", constants.PermRead, stdUser).Return(true)
		mockPerms.On("CheckObjectPermissionWithUser", mock.Anything, "SystemConfig", "ViewAllRecords", stdUser).Return(false)
		mockPerms.On("CheckFieldVisibilityWithUser", mock.Anything, "SystemConfig", "ConfigKey", stdUser).Return(true)
		mockPerms.On("BuildRLSResult", mock.Anything, mock.Anything, "SystemConfig", stdUser).Return(&permissions.RLSResult{Skip: true})

		mockMeta.On("GetSchema", mock.Anything, "SystemConfig").Return(configSchema)

		validator := analytics.NewSecurityValidator(mockPerms, mockMeta)

		sql := "SELECT ConfigKey FROM SystemConfig"
		rewritten, _, err := validator.ValidateAndRewrite(context.Background(), sql, nil, stdUser, nil)

		assert.NoError(t, err)
		assert.NotContains(t, rewritten, "owner_id")
	})

	// Complex query (Join) - RLS is now applied to each table in the JOIN
	t.Run("Complex Join - RLS applied to each table", func(t *testing.T) {
		mockPerms := new(MockPermissionChecker)
		mockMeta := new(MockMetadataProvider)

		// Permission checks traverse all tables
		mockPerms.On("CheckObjectPermissionWithUser", mock.Anything, "Account", constants.PermRead, stdUser).Return(true)
		mockPerms.On("CheckObjectPermissionWithUser", mock.Anything, "Contact", constants.PermRead, stdUser).Return(true)
		mockPerms.On("CheckObjectPermissionWithUser", mock.Anything, "Account", "ViewAllRecords", stdUser).Return(false)
		mockPerms.On("CheckObjectPermissionWithUser", mock.Anything, "Contact", "ViewAllRecords", stdUser).Return(false)
		// GetSubordinateUserIDs called for each table that needs RLS (user has no role)
		mockPerms.On("CheckFieldVisibilityWithUser", mock.Anything, "Account", constants.FieldID, stdUser).Return(true)
		mockPerms.On("CheckFieldVisibilityWithUser", mock.Anything, "Contact", "AccountId", stdUser).Return(true)

		// Both tables have owner_id
		mockMeta.On("GetSchema", mock.Anything, "Account").Return(accountSchema)
		contactSchema := &models.ObjectMetadata{
			APIName: "Contact",
			Fields: []models.FieldMetadata{
				{APIName: constants.FieldOwnerID, Type: constants.FieldTypeReference},
				{APIName: "AccountId", Type: constants.FieldTypeReference},
			},
		}
		mockMeta.On("GetSchema", mock.Anything, "Contact").Return(contactSchema)
		mockPerms.On("BuildRLSResult", mock.Anything, mock.Anything, "Account", stdUser).Return(&permissions.RLSResult{
			ObjectAPIName: "Account", HasOwnerField: true, UserID: stdUser.ID, OwnerUserIDs: []string{stdUser.ID},
		})
		mockPerms.On("BuildRLSResult", mock.Anything, mock.Anything, "Contact", stdUser).Return(&permissions.RLSResult{
			ObjectAPIName: "Contact", HasOwnerField: true, UserID: stdUser.ID, OwnerUserIDs: []string{stdUser.ID},
		})

		validator := analytics.NewSecurityValidator(mockPerms, mockMeta)

		sql := fmt.Sprintf("SELECT * FROM Account JOIN Contact ON Account.%s = Contact.AccountId", constants.FieldID)
		rewritten, _, err := validator.ValidateAndRewrite(context.Background(), sql, nil, stdUser, nil)

		assert.NoError(t, err)
		// RLS is now injected for JOIN queries too
		assert.Contains(t, rewritten, constants.FieldOwnerID)
		assert.Contains(t, rewritten, "user-123")
	})
}

func TestSecurityValidator_ContextVars(t *testing.T) {
	stdUser := &models.UserSession{
		ID:        "user-123",
		ProfileID: constants.ProfileSystemAdmin, // Admin to skip RLS
	}
	// BuildRLSResult returns Skip for admin across all tests in this group
	adminRLSSkip := &permissions.RLSResult{Skip: true}

	t.Run("ContextVars: Substitutes start_date and end_date", func(t *testing.T) {
		mockPerms := new(MockPermissionChecker)
		mockMeta := new(MockMetadataProvider)

		mockPerms.On("CheckObjectPermissionWithUser", mock.Anything, "Opportunity", constants.PermRead, stdUser).Return(true)
		mockPerms.On("CheckFieldVisibilityWithUser", mock.Anything, "Opportunity", constants.FieldCreatedDate, stdUser).Return(true)
		mockPerms.On("BuildRLSResult", mock.Anything, mock.Anything, mock.Anything, stdUser).Return(adminRLSSkip)
		mockMeta.On("GetSchema", mock.Anything, mock.Anything).Return(&models.ObjectMetadata{APIName: "generic"})

		validator := analytics.NewSecurityValidator(mockPerms, mockMeta)

		sql := fmt.Sprintf("SELECT * FROM Opportunity WHERE %s >= @start_date AND %s <= @end_date", constants.FieldCreatedDate, constants.FieldCreatedDate)
		contextVars := map[string]string{
			"start_date": "2025-01-01",
			"end_date":   "2025-12-31",
		}

		rewritten, _, err := validator.ValidateAndRewrite(context.Background(), sql, nil, stdUser, contextVars)

		assert.NoError(t, err)
		assert.Contains(t, rewritten, "'2025-01-01'")
		assert.Contains(t, rewritten, "'2025-12-31'")
		assert.NotContains(t, rewritten, "@start_date")
		assert.NotContains(t, rewritten, "@end_date")
	})

	t.Run("ContextVars: Unknown variable returns error", func(t *testing.T) {
		mockPerms := new(MockPermissionChecker)
		mockMeta := new(MockMetadataProvider)

		mockPerms.On("CheckObjectPermissionWithUser", mock.Anything, "Opportunity", constants.PermRead, stdUser).Return(true)
		mockPerms.On("CheckFieldVisibilityWithUser", mock.Anything, "Opportunity", constants.FieldName, stdUser).Return(true)
		mockPerms.On("BuildRLSResult", mock.Anything, mock.Anything, mock.Anything, stdUser).Return(adminRLSSkip)
		mockMeta.On("GetSchema", mock.Anything, mock.Anything).Return(&models.ObjectMetadata{APIName: "generic"})

		validator := analytics.NewSecurityValidator(mockPerms, mockMeta)

		sql := fmt.Sprintf("SELECT * FROM Opportunity WHERE %s = @unknown_var", constants.FieldName)
		contextVars := map[string]string{} // No unknown_var provided

		_, _, err := validator.ValidateAndRewrite(context.Background(), sql, nil, stdUser, contextVars)

		assert.Error(t, err)
		assert.Contains(t, err.Error(), "unknown SQL variable(s)")
		assert.Contains(t, err.Error(), "unknown_var")
	})

	t.Run("ContextVars: user_id cannot be overridden", func(t *testing.T) {
		mockPerms := new(MockPermissionChecker)
		mockMeta := new(MockMetadataProvider)

		mockPerms.On("CheckObjectPermissionWithUser", mock.Anything, "User", constants.PermRead, stdUser).Return(true)
		mockPerms.On("CheckFieldVisibilityWithUser", mock.Anything, "User", constants.FieldID, stdUser).Return(true)
		mockPerms.On("BuildRLSResult", mock.Anything, mock.Anything, mock.Anything, stdUser).Return(adminRLSSkip)
		mockMeta.On("GetSchema", mock.Anything, "User").Return(&models.ObjectMetadata{
			APIName: "User",
			Fields:  []models.FieldMetadata{{APIName: constants.FieldID}},
		})

		validator := analytics.NewSecurityValidator(mockPerms, mockMeta)

		sql := fmt.Sprintf("SELECT %s FROM User WHERE %s = @user_id", constants.FieldID, constants.FieldID)
		contextVars := map[string]string{
			"user_id": "attacker-id-000", // Attempt to override
		}

		rewritten, _, err := validator.ValidateAndRewrite(context.Background(), sql, nil, stdUser, contextVars)

		assert.NoError(t, err)
		// Should use session user_id, not the attacker's
		assert.Contains(t, rewritten, "'user-123'")
		assert.NotContains(t, rewritten, "attacker-id-000")
	})
}
