package services_test

import (
	"context"
	"encoding/json"
	"fmt"
	"path/filepath"
	"testing"
	"time"

	"github.com/Wikid82/charon/backend/internal/crypto"
	"github.com/Wikid82/charon/backend/internal/models"
	"github.com/Wikid82/charon/backend/internal/services"
	_ "github.com/Wikid82/charon/backend/pkg/dnsprovider/builtin" // Register built-in providers
	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func setupCredentialTestDB(t *testing.T) (*gorm.DB, *crypto.EncryptionService) {
	// Use a unique file-backed database to avoid in-memory connection isolation and lock contention.
	dsn := filepath.Join(t.TempDir(), fmt.Sprintf("%s.db", t.Name())) + "?_journal_mode=WAL&_busy_timeout=5000"
	db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
	require.NoError(t, err)

	sqlDB, err := db.DB()
	require.NoError(t, err)
	sqlDB.SetMaxOpenConns(1)
	sqlDB.SetMaxIdleConns(1)

	// Close database connection when test completes
	t.Cleanup(func() {
		_ = sqlDB.Close()
	})

	err = db.AutoMigrate(
		&models.DNSProvider{},
		&models.DNSProviderCredential{},
		&models.SecurityAudit{},
	)
	require.NoError(t, err)

	// Create encryption service with test key (32 bytes base64 encoded)
	testKey := "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=" // "0123456789abcdef0123456789abcdef" base64 encoded
	encryptor, err := crypto.NewEncryptionService(testKey)
	require.NoError(t, err)

	return db, encryptor
}

func createTestProvider(t *testing.T, db *gorm.DB, encryptor *crypto.EncryptionService, multiCred bool) *models.DNSProvider {
	creds := map[string]string{"api_token": "test-token"}
	credsJSON, _ := json.Marshal(creds)
	encrypted, _ := encryptor.Encrypt(credsJSON)

	provider := &models.DNSProvider{
		UUID:                 uuid.New().String(),
		Name:                 "Test Provider",
		ProviderType:         "cloudflare",
		Enabled:              true,
		UseMultiCredentials:  multiCred,
		CredentialsEncrypted: encrypted,
		KeyVersion:           1,
		PropagationTimeout:   120,
		PollingInterval:      5,
	}

	err := db.Create(provider).Error
	require.NoError(t, err)
	return provider
}

func TestCredentialService_Create(t *testing.T) {
	db, encryptor := setupCredentialTestDB(t)
	service := services.NewCredentialService(db, encryptor)
	ctx := context.Background()

	// Create provider with multi-credential enabled
	provider := createTestProvider(t, db, encryptor, true)

	req := services.CreateCredentialRequest{
		Label:      "Production Credential",
		ZoneFilter: "example.com",
		Credentials: map[string]string{
			"api_token": "prod-token-123",
		},
		PropagationTimeout: 180,
		PollingInterval:    10,
		Enabled:            true,
	}

	cred, err := service.Create(ctx, provider.ID, req)
	require.NoError(t, err)
	assert.NotNil(t, cred)
	assert.Equal(t, "Production Credential", cred.Label)
	assert.Equal(t, "example.com", cred.ZoneFilter)
	assert.Equal(t, provider.ID, cred.DNSProviderID)
	assert.Equal(t, 180, cred.PropagationTimeout)
	assert.Equal(t, 10, cred.PollingInterval)
	assert.True(t, cred.Enabled)
	assert.NotEmpty(t, cred.UUID)
	assert.NotEmpty(t, cred.CredentialsEncrypted)
}

func TestCredentialService_Create_MultiCredentialNotEnabled(t *testing.T) {
	db, encryptor := setupCredentialTestDB(t)
	service := services.NewCredentialService(db, encryptor)
	ctx := context.Background()

	// Create provider without multi-credential enabled
	provider := createTestProvider(t, db, encryptor, false)

	req := services.CreateCredentialRequest{
		Label:       "Test",
		Credentials: map[string]string{"api_token": "token"},
	}

	_, err := service.Create(ctx, provider.ID, req)
	assert.ErrorIs(t, err, services.ErrMultiCredentialNotEnabled)
}

func TestCredentialService_Create_InvalidCredentials(t *testing.T) {
	db, encryptor := setupCredentialTestDB(t)
	service := services.NewCredentialService(db, encryptor)
	ctx := context.Background()

	provider := createTestProvider(t, db, encryptor, true)

	req := services.CreateCredentialRequest{
		Label:       "Test",
		Credentials: map[string]string{}, // Missing required field
	}

	_, err := service.Create(ctx, provider.ID, req)
	assert.Error(t, err)
}

func TestCredentialService_List(t *testing.T) {
	db, encryptor := setupCredentialTestDB(t)
	service := services.NewCredentialService(db, encryptor)
	ctx := context.Background()

	provider := createTestProvider(t, db, encryptor, true)

	// Create multiple credentials with slight delay to avoid SQLite locking
	for i := 0; i < 3; i++ {
		req := services.CreateCredentialRequest{
			Label:       "Credential " + string(rune('A'+i)),
			ZoneFilter:  "",
			Credentials: map[string]string{"api_token": "token"},
		}
		_, err := service.Create(ctx, provider.ID, req)
		require.NoError(t, err)
		if i < 2 {
			time.Sleep(10 * time.Millisecond)
		}
	}

	creds, err := service.List(ctx, provider.ID)
	require.NoError(t, err)
	assert.Len(t, creds, 3)
}

func TestCredentialService_Get(t *testing.T) {
	db, encryptor := setupCredentialTestDB(t)
	service := services.NewCredentialService(db, encryptor)
	ctx := context.Background()

	provider := createTestProvider(t, db, encryptor, true)

	req := services.CreateCredentialRequest{
		Label:       "Test",
		Credentials: map[string]string{"api_token": "token"},
	}
	created, err := service.Create(ctx, provider.ID, req)
	require.NoError(t, err)

	cred, err := service.Get(ctx, provider.ID, created.ID)
	require.NoError(t, err)
	assert.Equal(t, created.ID, cred.ID)
	assert.Equal(t, created.Label, cred.Label)
}

func TestCredentialService_Get_NotFound(t *testing.T) {
	db, encryptor := setupCredentialTestDB(t)
	service := services.NewCredentialService(db, encryptor)
	ctx := context.Background()

	provider := createTestProvider(t, db, encryptor, true)

	_, err := service.Get(ctx, provider.ID, 9999)
	assert.ErrorIs(t, err, services.ErrCredentialNotFound)
}

func TestCredentialService_Update(t *testing.T) {
	db, encryptor := setupCredentialTestDB(t)
	service := services.NewCredentialService(db, encryptor)
	ctx := context.Background()

	provider := createTestProvider(t, db, encryptor, true)

	req := services.CreateCredentialRequest{
		Label:       "Original",
		ZoneFilter:  "example.com",
		Credentials: map[string]string{"api_token": "token"},
	}
	created, err := service.Create(ctx, provider.ID, req)
	require.NoError(t, err)

	newLabel := "Updated Label"
	newZone := "*.example.com"
	enabled := false
	updateReq := services.UpdateCredentialRequest{
		Label:      &newLabel,
		ZoneFilter: &newZone,
		Enabled:    &enabled,
	}

	updated, err := service.Update(ctx, provider.ID, created.ID, updateReq)
	require.NoError(t, err)
	assert.Equal(t, "Updated Label", updated.Label)
	assert.Equal(t, "*.example.com", updated.ZoneFilter)
	assert.False(t, updated.Enabled)
}

func TestCredentialService_Delete(t *testing.T) {
	db, encryptor := setupCredentialTestDB(t)
	service := services.NewCredentialService(db, encryptor)
	ctx := context.Background()

	provider := createTestProvider(t, db, encryptor, true)

	req := services.CreateCredentialRequest{
		Label:       "To Delete",
		Credentials: map[string]string{"api_token": "token"},
	}
	created, err := service.Create(ctx, provider.ID, req)
	require.NoError(t, err)

	err = service.Delete(ctx, provider.ID, created.ID)
	require.NoError(t, err)

	_, err = service.Get(ctx, provider.ID, created.ID)
	assert.ErrorIs(t, err, services.ErrCredentialNotFound)
}

func TestCredentialService_Test(t *testing.T) {
	db, encryptor := setupCredentialTestDB(t)
	service := services.NewCredentialService(db, encryptor)
	ctx := context.Background()

	provider := createTestProvider(t, db, encryptor, true)

	req := services.CreateCredentialRequest{
		Label:       "Test",
		Credentials: map[string]string{"api_token": "token"},
	}
	created, err := service.Create(ctx, provider.ID, req)
	require.NoError(t, err)

	result, err := service.Test(ctx, provider.ID, created.ID)
	require.NoError(t, err)
	assert.NotNil(t, result)
	// Note: Actual test will depend on testDNSProviderCredentials implementation
}

func TestCredentialService_GetCredentialForDomain_ExactMatch(t *testing.T) {
	db, encryptor := setupCredentialTestDB(t)
	service := services.NewCredentialService(db, encryptor)
	ctx := context.Background()

	provider := createTestProvider(t, db, encryptor, true)

	// Create exact match credential
	req := services.CreateCredentialRequest{
		Label:       "Exact Match",
		ZoneFilter:  "example.com",
		Credentials: map[string]string{"api_token": "exact-token"},
	}
	exactCred, err := service.Create(ctx, provider.ID, req)
	require.NoError(t, err)

	// Create catch-all credential
	req2 := services.CreateCredentialRequest{
		Label:       "Catch All",
		ZoneFilter:  "",
		Credentials: map[string]string{"api_token": "catchall-token"},
	}
	_, err = service.Create(ctx, provider.ID, req2)
	require.NoError(t, err)

	// Test exact match
	cred, err := service.GetCredentialForDomain(ctx, provider.ID, "example.com")
	require.NoError(t, err)
	assert.Equal(t, exactCred.ID, cred.ID)
	assert.Equal(t, "Exact Match", cred.Label)
}

func TestCredentialService_GetCredentialForDomain_WildcardMatch(t *testing.T) {
	db, encryptor := setupCredentialTestDB(t)
	service := services.NewCredentialService(db, encryptor)
	ctx := context.Background()

	provider := createTestProvider(t, db, encryptor, true)

	// Create wildcard credential
	req := services.CreateCredentialRequest{
		Label:       "Wildcard",
		ZoneFilter:  "*.example.com",
		Credentials: map[string]string{"api_token": "wildcard-token"},
	}
	wildcardCred, err := service.Create(ctx, provider.ID, req)
	require.NoError(t, err)

	// Create catch-all
	req2 := services.CreateCredentialRequest{
		Label:       "Catch All",
		ZoneFilter:  "",
		Credentials: map[string]string{"api_token": "catchall-token"},
	}
	_, err = service.Create(ctx, provider.ID, req2)
	require.NoError(t, err)

	// Test wildcard match
	cred, err := service.GetCredentialForDomain(ctx, provider.ID, "app.example.com")
	require.NoError(t, err)
	assert.Equal(t, wildcardCred.ID, cred.ID)
	assert.Equal(t, "Wildcard", cred.Label)
}

func TestCredentialService_GetCredentialForDomain_CatchAll(t *testing.T) {
	db, encryptor := setupCredentialTestDB(t)
	service := services.NewCredentialService(db, encryptor)
	ctx := context.Background()

	provider := createTestProvider(t, db, encryptor, true)

	// Create catch-all credential
	req := services.CreateCredentialRequest{
		Label:       "Catch All",
		ZoneFilter:  "",
		Credentials: map[string]string{"api_token": "catchall-token"},
	}
	catchallCred, err := service.Create(ctx, provider.ID, req)
	require.NoError(t, err)

	// Test catch-all match
	cred, err := service.GetCredentialForDomain(ctx, provider.ID, "random.domain.com")
	require.NoError(t, err)
	assert.Equal(t, catchallCred.ID, cred.ID)
	assert.Equal(t, "Catch All", cred.Label)
}

func TestCredentialService_GetCredentialForDomain_NoMatch(t *testing.T) {
	db, encryptor := setupCredentialTestDB(t)
	service := services.NewCredentialService(db, encryptor)
	ctx := context.Background()

	provider := createTestProvider(t, db, encryptor, true)

	// Create specific credential without catch-all
	req := services.CreateCredentialRequest{
		Label:       "Specific",
		ZoneFilter:  "example.com",
		Credentials: map[string]string{"api_token": "token"},
	}
	_, err := service.Create(ctx, provider.ID, req)
	require.NoError(t, err)

	// Test no match
	_, err = service.GetCredentialForDomain(ctx, provider.ID, "other.com")
	assert.ErrorIs(t, err, services.ErrNoMatchingCredential)
}

func TestCredentialService_GetCredentialForDomain_MultiCredNotEnabled(t *testing.T) {
	db, encryptor := setupCredentialTestDB(t)
	service := services.NewCredentialService(db, encryptor)
	ctx := context.Background()

	// Create provider without multi-credential enabled
	provider := createTestProvider(t, db, encryptor, false)

	cred, err := service.GetCredentialForDomain(ctx, provider.ID, "example.com")
	require.NoError(t, err)
	assert.Nil(t, cred) // Should return nil when not using multi-credentials
}

func TestCredentialService_GetCredentialForDomain_MultipleZones(t *testing.T) {
	db, encryptor := setupCredentialTestDB(t)
	service := services.NewCredentialService(db, encryptor)
	ctx := context.Background()

	provider := createTestProvider(t, db, encryptor, true)

	// Create credential with multiple zones
	req := services.CreateCredentialRequest{
		Label:       "Multi-Zone",
		ZoneFilter:  "example.com,example.org",
		Credentials: map[string]string{"api_token": "multi-token"},
	}
	multiCred, err := service.Create(ctx, provider.ID, req)
	require.NoError(t, err)

	// Test first zone
	cred1, err := service.GetCredentialForDomain(ctx, provider.ID, "example.com")
	require.NoError(t, err)
	assert.Equal(t, multiCred.ID, cred1.ID)

	// Test second zone
	cred2, err := service.GetCredentialForDomain(ctx, provider.ID, "example.org")
	require.NoError(t, err)
	assert.Equal(t, multiCred.ID, cred2.ID)
}

func TestCredentialService_EnableMultiCredentials(t *testing.T) {
	db, encryptor := setupCredentialTestDB(t)
	service := services.NewCredentialService(db, encryptor)
	ctx := context.Background()

	// Create provider with credentials but multi-cred disabled
	provider := createTestProvider(t, db, encryptor, false)

	err := service.EnableMultiCredentials(ctx, provider.ID)
	require.NoError(t, err)

	// Verify provider is now in multi-credential mode
	var updatedProvider models.DNSProvider
	err = db.Where("id = ?", provider.ID).First(&updatedProvider).Error
	require.NoError(t, err)
	assert.True(t, updatedProvider.UseMultiCredentials)

	// Verify migrated credential was created
	var creds []models.DNSProviderCredential
	err = db.Where("dns_provider_id = ?", provider.ID).Find(&creds).Error
	require.NoError(t, err)
	assert.Len(t, creds, 1)
	assert.Equal(t, "Default (migrated)", creds[0].Label)
	assert.Equal(t, "", creds[0].ZoneFilter) // Catch-all
}

func TestCredentialService_EnableMultiCredentials_AlreadyEnabled(t *testing.T) {
	db, encryptor := setupCredentialTestDB(t)
	service := services.NewCredentialService(db, encryptor)
	ctx := context.Background()

	// Create provider with multi-cred already enabled
	provider := createTestProvider(t, db, encryptor, true)

	err := service.EnableMultiCredentials(ctx, provider.ID)
	require.NoError(t, err) // Should not error
}

func TestCredentialService_EnableMultiCredentials_NoCredentials(t *testing.T) {
	db, encryptor := setupCredentialTestDB(t)
	service := services.NewCredentialService(db, encryptor)
	ctx := context.Background()

	// Create provider without credentials
	provider := &models.DNSProvider{
		UUID:                "test-uuid",
		Name:                "Empty Provider",
		ProviderType:        "cloudflare",
		Enabled:             true,
		UseMultiCredentials: false,
		KeyVersion:          1,
	}
	err := db.Create(provider).Error
	require.NoError(t, err)

	err = service.EnableMultiCredentials(ctx, provider.ID)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "no credentials to migrate")
}

func TestCredentialService_GetCredentialForDomain_IDN(t *testing.T) {
	db, encryptor := setupCredentialTestDB(t)
	service := services.NewCredentialService(db, encryptor)
	ctx := context.Background()

	provider := createTestProvider(t, db, encryptor, true)

	// Create credential for IDN domain (punycode representation)
	req := services.CreateCredentialRequest{
		Label:       "IDN Domain",
		ZoneFilter:  "xn--e1afmkfd.xn--p1ai", // пример.рф in punycode
		Credentials: map[string]string{"api_token": "idn-token"},
	}
	idnCred, err := service.Create(ctx, provider.ID, req)
	require.NoError(t, err)

	// Test IDN match
	cred, err := service.GetCredentialForDomain(ctx, provider.ID, "xn--e1afmkfd.xn--p1ai")
	require.NoError(t, err)
	assert.Equal(t, idnCred.ID, cred.ID)
}
