package services

import (
	"crypto/sha256"
	"os"
	"testing"
	"time"

	"github.com/Wikid82/charon/backend/internal/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/crypto/bcrypt"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func setupEmergencyTokenTestDB(t *testing.T) *gorm.DB {
	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	require.NoError(t, err)

	err = db.AutoMigrate(&models.EmergencyToken{})
	require.NoError(t, err)

	return db
}

func TestEmergencyTokenService_Generate(t *testing.T) {
	tests := []struct {
		name           string
		expirationDays int
		expectedPolicy string
	}{
		{
			name:           "30 days policy",
			expirationDays: 30,
			expectedPolicy: "30_days",
		},
		{
			name:           "60 days policy",
			expirationDays: 60,
			expectedPolicy: "60_days",
		},
		{
			name:           "90 days policy",
			expirationDays: 90,
			expectedPolicy: "90_days",
		},
		{
			name:           "custom 45 days policy",
			expirationDays: 45,
			expectedPolicy: "custom_45_days",
		},
		{
			name:           "never expires",
			expirationDays: 0,
			expectedPolicy: "never",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			db := setupEmergencyTokenTestDB(t)
			svc := NewEmergencyTokenService(db)

			userID := uint(1)
			resp, err := svc.Generate(GenerateRequest{
				ExpirationDays: tt.expirationDays,
				UserID:         &userID,
			})

			require.NoError(t, err)
			assert.NotEmpty(t, resp.Token)
			assert.Equal(t, tt.expectedPolicy, resp.ExpirationPolicy)

			// Token should be 128 hex characters (64 bytes)
			assert.Len(t, resp.Token, 128)

			// Verify expiration
			if tt.expirationDays > 0 {
				assert.NotNil(t, resp.ExpiresAt)
				expectedExpiry := time.Now().Add(time.Duration(tt.expirationDays) * 24 * time.Hour)
				assert.WithinDuration(t, expectedExpiry, *resp.ExpiresAt, time.Minute)
			} else {
				assert.Nil(t, resp.ExpiresAt)
			}

			// Verify database record
			var tokenRecord models.EmergencyToken
			err = db.First(&tokenRecord).Error
			require.NoError(t, err)
			assert.Equal(t, tt.expectedPolicy, tokenRecord.ExpirationPolicy)

			// Verify bcrypt hash (not plaintext)
			tokenHash := sha256.Sum256([]byte(resp.Token))
			err = bcrypt.CompareHashAndPassword([]byte(tokenRecord.TokenHash), tokenHash[:])
			assert.NoError(t, err, "Token should be stored as bcrypt hash")
		})
	}
}

func TestEmergencyTokenService_Generate_ReplacesOldToken(t *testing.T) {
	db := setupEmergencyTokenTestDB(t)
	svc := NewEmergencyTokenService(db)

	// Generate first token
	resp1, err := svc.Generate(GenerateRequest{ExpirationDays: 90})
	require.NoError(t, err)

	// Generate second token
	resp2, err := svc.Generate(GenerateRequest{ExpirationDays: 60})
	require.NoError(t, err)

	// Verify tokens are different
	assert.NotEqual(t, resp1.Token, resp2.Token)

	// Verify only one token in database
	var count int64
	db.Model(&models.EmergencyToken{}).Count(&count)
	assert.Equal(t, int64(1), count)

	// Verify old token no longer validates
	_, err = svc.Validate(resp1.Token)
	assert.Error(t, err)

	// Verify new token validates
	_, err = svc.Validate(resp2.Token)
	assert.NoError(t, err)
}

func TestEmergencyTokenService_Validate(t *testing.T) {
	db := setupEmergencyTokenTestDB(t)
	svc := NewEmergencyTokenService(db)

	// Generate token
	resp, err := svc.Generate(GenerateRequest{ExpirationDays: 90})
	require.NoError(t, err)

	tests := []struct {
		name        string
		token       string
		expectError bool
		errorMsg    string
	}{
		{
			name:        "valid token",
			token:       resp.Token,
			expectError: false,
		},
		{
			name:        "invalid token",
			token:       "invalid-token-12345",
			expectError: true,
			errorMsg:    "invalid token",
		},
		{
			name:        "empty token",
			token:       "",
			expectError: true,
			errorMsg:    "token is empty",
		},
		{
			name:        "whitespace token",
			token:       "   ",
			expectError: true,
			errorMsg:    "token is empty",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tokenRecord, err := svc.Validate(tt.token)

			if tt.expectError {
				assert.Error(t, err)
				if tt.errorMsg != "" {
					assert.Contains(t, err.Error(), tt.errorMsg)
				}
				assert.Nil(t, tokenRecord)
			} else {
				assert.NoError(t, err)
				assert.NotNil(t, tokenRecord)
				assert.Greater(t, tokenRecord.UseCount, 0)
				assert.NotNil(t, tokenRecord.LastUsedAt)
			}
		})
	}
}

func TestEmergencyTokenService_Validate_Expiration(t *testing.T) {
	db := setupEmergencyTokenTestDB(t)
	svc := NewEmergencyTokenService(db)

	// Generate token with short expiration
	resp, err := svc.Generate(GenerateRequest{ExpirationDays: 1})
	require.NoError(t, err)

	// Manually expire the token
	var tokenRecord models.EmergencyToken
	db.First(&tokenRecord)
	past := time.Now().Add(-25 * time.Hour)
	tokenRecord.ExpiresAt = &past
	db.Save(&tokenRecord)

	// Validate should fail
	_, err = svc.Validate(resp.Token)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "expired")
}

func TestEmergencyTokenService_Validate_EnvironmentFallback(t *testing.T) {
	db := setupEmergencyTokenTestDB(t)
	svc := NewEmergencyTokenService(db)

	// Set environment variable
	envToken := "this-is-a-long-test-token-for-environment-fallback-validation"
	os.Setenv(EmergencyTokenEnvVar, envToken)
	defer os.Unsetenv(EmergencyTokenEnvVar)

	// Validate with environment token (no DB token exists)
	tokenRecord, err := svc.Validate(envToken)
	assert.NoError(t, err)
	assert.Nil(t, tokenRecord, "Env var tokens return nil record")
}

func TestEmergencyTokenService_Validate_DatabaseTakesPrecedence(t *testing.T) {
	db := setupEmergencyTokenTestDB(t)
	svc := NewEmergencyTokenService(db)

	// Set environment variable
	envToken := "this-is-a-long-test-token-for-environment-fallback-validation"
	os.Setenv(EmergencyTokenEnvVar, envToken)
	defer os.Unsetenv(EmergencyTokenEnvVar)

	// Generate database token
	dbResp, err := svc.Generate(GenerateRequest{ExpirationDays: 90})
	require.NoError(t, err)

	// Database token should validate
	_, err = svc.Validate(dbResp.Token)
	assert.NoError(t, err)

	// Environment token should NOT validate (database takes precedence)
	_, err = svc.Validate(envToken)
	assert.Error(t, err)
}

func TestEmergencyTokenService_GetStatus(t *testing.T) {
	t.Run("no token configured", func(t *testing.T) {
		db := setupEmergencyTokenTestDB(t)
		svc := NewEmergencyTokenService(db)

		status, err := svc.GetStatus()
		require.NoError(t, err)

		assert.False(t, status.Configured)
		assert.Equal(t, "none", status.Source)
		assert.Nil(t, status.CreatedAt)
		assert.Nil(t, status.ExpiresAt)
	})

	t.Run("database token configured", func(t *testing.T) {
		db := setupEmergencyTokenTestDB(t)
		svc := NewEmergencyTokenService(db)

		// Generate token
		resp, err := svc.Generate(GenerateRequest{ExpirationDays: 90})
		require.NoError(t, err)

		// Get status
		status, err := svc.GetStatus()
		require.NoError(t, err)

		assert.True(t, status.Configured)
		assert.Equal(t, "database", status.Source)
		assert.NotNil(t, status.CreatedAt)
		assert.NotNil(t, status.ExpiresAt)
		assert.Equal(t, "90_days", status.ExpirationPolicy)
		assert.False(t, status.IsExpired)
		assert.Greater(t, status.DaysUntilExpiration, 85)

		// Validate token to update usage
		_, err = svc.Validate(resp.Token)
		require.NoError(t, err)

		// Check updated status
		status, err = svc.GetStatus()
		require.NoError(t, err)
		assert.Equal(t, 1, status.UseCount)
		assert.NotNil(t, status.LastUsedAt)
	})

	t.Run("environment token configured", func(t *testing.T) {
		db := setupEmergencyTokenTestDB(t)
		svc := NewEmergencyTokenService(db)

		// Set environment variable
		envToken := "this-is-a-long-test-token-for-environment-configuration"
		os.Setenv(EmergencyTokenEnvVar, envToken)
		defer os.Unsetenv(EmergencyTokenEnvVar)

		// Get status
		status, err := svc.GetStatus()
		require.NoError(t, err)

		assert.True(t, status.Configured)
		assert.Equal(t, "environment", status.Source)
		assert.Equal(t, "never", status.ExpirationPolicy)
		assert.Equal(t, -1, status.DaysUntilExpiration)
		assert.False(t, status.IsExpired)
	})
}

func TestEmergencyTokenService_Revoke(t *testing.T) {
	db := setupEmergencyTokenTestDB(t)
	svc := NewEmergencyTokenService(db)

	// Generate token
	resp, err := svc.Generate(GenerateRequest{ExpirationDays: 90})
	require.NoError(t, err)

	// Revoke token
	err = svc.Revoke()
	assert.NoError(t, err)

	// Verify token no longer validates
	_, err = svc.Validate(resp.Token)
	assert.Error(t, err)

	// Verify no token configured
	status, err := svc.GetStatus()
	require.NoError(t, err)
	assert.False(t, status.Configured)
}

func TestEmergencyTokenService_Revoke_NoToken(t *testing.T) {
	db := setupEmergencyTokenTestDB(t)
	svc := NewEmergencyTokenService(db)

	// Attempt to revoke when no token exists
	err := svc.Revoke()
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "no token to revoke")
}

func TestEmergencyTokenService_UpdateExpiration(t *testing.T) {
	db := setupEmergencyTokenTestDB(t)
	svc := NewEmergencyTokenService(db)

	// Generate token with 90 days
	resp, err := svc.Generate(GenerateRequest{ExpirationDays: 90})
	require.NoError(t, err)

	// Update to 30 days
	newExpiresAt, err := svc.UpdateExpiration(30)
	require.NoError(t, err)
	assert.NotNil(t, newExpiresAt)

	// Verify updated expiration
	status, err := svc.GetStatus()
	require.NoError(t, err)
	assert.Equal(t, "30_days", status.ExpirationPolicy)
	assert.Greater(t, status.DaysUntilExpiration, 25)
	assert.Less(t, status.DaysUntilExpiration, 31)

	// Token should still validate
	_, err = svc.Validate(resp.Token)
	assert.NoError(t, err)
}

func TestEmergencyTokenService_UpdateExpiration_ToNever(t *testing.T) {
	db := setupEmergencyTokenTestDB(t)
	svc := NewEmergencyTokenService(db)

	// Generate token with 30 days
	resp, err := svc.Generate(GenerateRequest{ExpirationDays: 30})
	require.NoError(t, err)

	// Update to never expire
	newExpiresAt, err := svc.UpdateExpiration(0)
	require.NoError(t, err)
	assert.Nil(t, newExpiresAt)

	// Verify never expires
	status, err := svc.GetStatus()
	require.NoError(t, err)
	assert.Equal(t, "never", status.ExpirationPolicy)
	assert.Equal(t, -1, status.DaysUntilExpiration)
	assert.False(t, status.IsExpired)

	// Token should still validate
	_, err = svc.Validate(resp.Token)
	assert.NoError(t, err)
}

func TestEmergencyTokenService_UpdateExpiration_NoToken(t *testing.T) {
	db := setupEmergencyTokenTestDB(t)
	svc := NewEmergencyTokenService(db)

	// Attempt to update when no token exists
	_, err := svc.UpdateExpiration(60)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "no token found")
}

func TestEmergencyToken_IsExpired(t *testing.T) {
	tests := []struct {
		name      string
		expiresAt *time.Time
		isExpired bool
	}{
		{
			name:      "never expires",
			expiresAt: nil,
			isExpired: false,
		},
		{
			name:      "expires in future",
			expiresAt: func() *time.Time { t := time.Now().Add(24 * time.Hour); return &t }(),
			isExpired: false,
		},
		{
			name:      "expires in past",
			expiresAt: func() *time.Time { t := time.Now().Add(-24 * time.Hour); return &t }(),
			isExpired: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			token := &models.EmergencyToken{
				ExpiresAt: tt.expiresAt,
			}
			assert.Equal(t, tt.isExpired, token.IsExpired())
		})
	}
}

func TestEmergencyToken_DaysUntilExpiration(t *testing.T) {
	tests := []struct {
		name         string
		expiresAt    *time.Time
		expectedDays int
	}{
		{
			name:         "never expires",
			expiresAt:    nil,
			expectedDays: -1,
		},
		{
			name:         "expires in 10 days",
			expiresAt:    func() *time.Time { t := time.Now().Add(10 * 24 * time.Hour); return &t }(),
			expectedDays: 10,
		},
		{
			name:         "expired 5 days ago",
			expiresAt:    func() *time.Time { t := time.Now().Add(-5 * 24 * time.Hour); return &t }(),
			expectedDays: -5,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			token := &models.EmergencyToken{
				ExpiresAt: tt.expiresAt,
			}
			days := token.DaysUntilExpiration()
			// Allow +/- 1 day for test timing variations
			assert.InDelta(t, float64(tt.expectedDays), float64(days), 1.0)
		})
	}
}
