package secret

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"filippo.io/age"
	"git.eeqj.de/sneak/secret/pkg/agehd"
	"github.com/spf13/afero"
	"github.com/stretchr/testify/require"
)

// MockVault is a test implementation of the VaultInterface
type MockVault struct {
	name            string
	fs              afero.Fs
	directory       string
	derivationIndex uint32
}

func (m *MockVault) GetDirectory() (string, error) {
	return m.directory, nil
}

func (m *MockVault) AddSecret(name string, value []byte, force bool) error {
	// Create secret directory with proper storage name conversion
	storageName := strings.ReplaceAll(name, "/", "%")
	secretDir := filepath.Join(m.directory, "secrets.d", storageName)
	if err := m.fs.MkdirAll(secretDir, 0700); err != nil {
		return err
	}

	// Create version directory with proper path
	versionName := "20240101.001" // Use a fixed version name for testing
	versionDir := filepath.Join(secretDir, "versions", versionName)
	if err := m.fs.MkdirAll(versionDir, 0700); err != nil {
		return err
	}

	// Read the vault's long-term public key
	ltPubKeyPath := filepath.Join(m.directory, "pub.age")

	// Derive long-term key using the vault's derivation index
	mnemonic := os.Getenv(EnvMnemonic)
	if mnemonic == "" {
		return fmt.Errorf("SB_SECRET_MNEMONIC not set")
	}

	ltIdentity, err := agehd.DeriveIdentity(mnemonic, m.derivationIndex)
	if err != nil {
		return err
	}

	// Write long-term public key if it doesn't exist
	if _, err := m.fs.Stat(ltPubKeyPath); os.IsNotExist(err) {
		pubKey := ltIdentity.Recipient().String()
		if err := afero.WriteFile(m.fs, ltPubKeyPath, []byte(pubKey), 0600); err != nil {
			return err
		}
	}

	// Generate version-specific keypair
	versionIdentity, err := age.GenerateX25519Identity()
	if err != nil {
		return err
	}

	// Write version public key
	pubKeyPath := filepath.Join(versionDir, "pub.age")
	if err := afero.WriteFile(m.fs, pubKeyPath, []byte(versionIdentity.Recipient().String()), 0600); err != nil {
		return err
	}

	// Encrypt value to version's public key
	encryptedValue, err := EncryptToRecipient(value, versionIdentity.Recipient())
	if err != nil {
		return err
	}

	// Write encrypted value
	valuePath := filepath.Join(versionDir, "value.age")
	if err := afero.WriteFile(m.fs, valuePath, encryptedValue, 0600); err != nil {
		return err
	}

	// Encrypt version private key to long-term public key
	encryptedPrivKey, err := EncryptToRecipient([]byte(versionIdentity.String()), ltIdentity.Recipient())
	if err != nil {
		return err
	}

	// Write encrypted version private key
	privKeyPath := filepath.Join(versionDir, "priv.age")
	if err := afero.WriteFile(m.fs, privKeyPath, encryptedPrivKey, 0600); err != nil {
		return err
	}

	// Create current symlink pointing to the version
	currentLink := filepath.Join(secretDir, "current")
	// For MemMapFs, write a file with the target path
	if err := afero.WriteFile(m.fs, currentLink, []byte("versions/"+versionName), 0600); err != nil {
		return err
	}

	return nil
}

func (m *MockVault) GetName() string {
	return m.name
}

func (m *MockVault) GetFilesystem() afero.Fs {
	return m.fs
}

func (m *MockVault) GetCurrentUnlocker() (Unlocker, error) {
	return nil, nil
}

func (m *MockVault) CreatePassphraseUnlocker(passphrase string) (*PassphraseUnlocker, error) {
	return nil, nil
}

func TestPerSecretKeyFunctionality(t *testing.T) {
	// Create an in-memory filesystem for testing
	fs := afero.NewMemMapFs()

	// Set up test environment variables
	oldMnemonic := os.Getenv(EnvMnemonic)
	defer func() {
		if oldMnemonic == "" {
			os.Unsetenv(EnvMnemonic)
		} else {
			os.Setenv(EnvMnemonic, oldMnemonic)
		}
	}()

	// Set test mnemonic for direct encryption/decryption
	testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
	os.Setenv(EnvMnemonic, testMnemonic)

	// Set up a test vault structure
	baseDir := "/test-config/berlin.sneak.pkg.secret"
	vaultDir := filepath.Join(baseDir, "vaults.d", "test-vault")

	// Create vault directory structure
	err := fs.MkdirAll(filepath.Join(vaultDir, "secrets.d"), DirPerms)
	if err != nil {
		t.Fatalf("Failed to create vault directory: %v", err)
	}

	// Generate a long-term keypair for the vault using the test mnemonic
	ltIdentity, err := agehd.DeriveIdentity(testMnemonic, 0)
	if err != nil {
		t.Fatalf("Failed to generate long-term identity: %v", err)
	}

	// Write long-term public key
	ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
	err = afero.WriteFile(
		fs,
		ltPubKeyPath,
		[]byte(ltIdentity.Recipient().String()),
		0600,
	)
	if err != nil {
		t.Fatalf("Failed to write long-term public key: %v", err)
	}

	// Set current vault
	currentVaultPath := filepath.Join(baseDir, "currentvault")
	err = afero.WriteFile(fs, currentVaultPath, []byte(vaultDir), FilePerms)
	if err != nil {
		t.Fatalf("Failed to set current vault: %v", err)
	}

	// Create vault instance using the mock vault
	vault := &MockVault{
		name:            "test-vault",
		fs:              fs,
		directory:       vaultDir,
		derivationIndex: 0,
	}

	// Test data
	secretName := "test-secret"
	secretValue := []byte("this is a test secret value")

	// Test AddSecret
	t.Run("AddSecret", func(t *testing.T) {
		err := vault.AddSecret(secretName, secretValue, false)
		if err != nil {
			t.Fatalf("AddSecret failed: %v", err)
		}

		// Verify that all expected files were created
		secretDir := filepath.Join(vaultDir, "secrets.d", secretName)

		// Check versions directory exists
		versionsDir := filepath.Join(secretDir, "versions")
		versionsDirExists, err := afero.DirExists(fs, versionsDir)
		if err != nil || !versionsDirExists {
			t.Fatalf("versions directory was not created")
		}

		// Check current symlink exists
		currentVersion, err := GetCurrentVersion(fs, secretDir)
		if err != nil {
			t.Fatalf("Failed to get current version: %v", err)
		}

		// Check value.age exists in the version directory
		versionDir := filepath.Join(versionsDir, currentVersion)
		valueExists, err := afero.Exists(
			fs,
			filepath.Join(versionDir, "value.age"),
		)
		if err != nil || !valueExists {
			t.Fatalf("value.age file was not created in version directory")
		}

		t.Logf("All expected files created successfully with versioning")
	})

	// Create a Secret object to test with
	secret := NewSecret(vault, secretName)

	// Test GetValue (this will need to be modified since we're using a mock vault)
	t.Run("GetSecret", func(t *testing.T) {
		// This test is simplified since we're not implementing the full encryption/decryption
		// in the mock. We just verify the Secret object is created correctly.
		if secret.Name != secretName {
			t.Fatalf("Secret name doesn't match. Expected: %s, Got: %s", secretName, secret.Name)
		}

		if secret.vault != vault {
			t.Fatalf("Secret vault reference doesn't match expected vault")
		}

		t.Logf("Successfully created Secret object with correct properties")
	})

	// Test Exists
	t.Run("SecretExists", func(t *testing.T) {
		exists, err := secret.Exists()
		if err != nil {
			t.Fatalf("Error checking if secret exists: %v", err)
		}
		if !exists {
			t.Fatalf("Secret should exist but Exists() returned false")
		}
		t.Logf("Secret.Exists() works correctly")
	})
}

// For testing purposes only
func isValidSecretName(name string) bool {
	if name == "" {
		return false
	}
	// Valid characters for secret names: lowercase letters, numbers, dash, dot, underscore, slash
	for _, char := range name {
		if (char < 'a' || char > 'z') && // lowercase letters
			(char < '0' || char > '9') && // numbers
			char != '-' && // dash
			char != '.' && // dot
			char != '_' && // underscore
			char != '/' { // slash
			return false
		}
	}
	return true
}

func TestSecretNameValidation(t *testing.T) {
	tests := []struct {
		name  string
		valid bool
	}{
		{"valid-name", true},
		{"valid.name", true},
		{"valid_name", true},
		{"valid/path/name", true},
		{"123valid", true},
		{"", false},
		{"Invalid-Name", false}, // uppercase not allowed
		{"invalid name", false}, // space not allowed
		{"invalid@name", false}, // @ not allowed
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			result := isValidSecretName(test.name)
			if result != test.valid {
				t.Errorf(
					"isValidSecretName(%q) = %v, want %v",
					test.name,
					result,
					test.valid,
				)
			}
		})
	}
}

func TestSecretGetValueWithEnvMnemonicUsesVaultDerivationIndex(t *testing.T) {
	// This test demonstrates the bug where GetValue uses hardcoded index 0
	// instead of the vault's actual derivation index when using environment mnemonic

	// Set up test mnemonic
	testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
	originalEnv := os.Getenv(EnvMnemonic)
	os.Setenv(EnvMnemonic, testMnemonic)
	defer os.Setenv(EnvMnemonic, originalEnv)

	// Create temporary directory for vaults
	fs := afero.NewOsFs()
	tempDir, err := afero.TempDir(fs, "", "secret-test-")
	require.NoError(t, err)
	defer func() {
		_ = fs.RemoveAll(tempDir)
	}()

	stateDir := filepath.Join(tempDir, ".secret")
	require.NoError(t, fs.MkdirAll(stateDir, 0700))

	// This test is now in the integration test file where it can use real vaults
	// The bug is demonstrated there - see test31EnvMnemonicUsesVaultDerivationIndex
	t.Log("This test demonstrates the bug in the integration test file")
}
