package vault

import (
	"path/filepath"
	"testing"
	"time"

	"git.eeqj.de/sneak/secret/internal/secret"
	"git.eeqj.de/sneak/secret/pkg/agehd"
	"github.com/spf13/afero"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// Helper function to create a vault with long-term key set up
func createTestVaultWithKey(t *testing.T, fs afero.Fs, stateDir, vaultName string) *Vault {
	// Set mnemonic for testing
	t.Setenv(secret.EnvMnemonic, "abandon abandon abandon abandon abandon abandon abandon abandon abandon about")

	// Create vault
	vault, err := CreateVault(fs, stateDir, vaultName)
	require.NoError(t, err)

	// Derive and store long-term key from mnemonic
	mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
	ltIdentity, err := agehd.DeriveIdentity(mnemonic, 0)
	require.NoError(t, err)

	// Store long-term public key in vault
	vaultDir, _ := vault.GetDirectory()
	ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
	err = afero.WriteFile(fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0600)
	require.NoError(t, err)

	// Unlock the vault with the derived key
	vault.Unlock(ltIdentity)

	return vault
}

func TestVaultAddSecretCreatesVersion(t *testing.T) {
	fs := afero.NewMemMapFs()
	stateDir := "/test/state"

	// Create vault with long-term key
	vault := createTestVaultWithKey(t, fs, stateDir, "test")

	// Add a secret
	secretName := "test/secret"
	secretValue := []byte("initial-value")

	err := vault.AddSecret(secretName, secretValue, false)
	require.NoError(t, err)

	// Check that version directory was created
	vaultDir, _ := vault.GetDirectory()
	secretDir := vaultDir + "/secrets.d/test%secret"
	versionsDir := secretDir + "/versions"

	// Should have one version
	entries, err := afero.ReadDir(fs, versionsDir)
	require.NoError(t, err)
	assert.Len(t, entries, 1)

	// Should have current symlink
	currentPath := secretDir + "/current"
	exists, err := afero.Exists(fs, currentPath)
	require.NoError(t, err)
	assert.True(t, exists)

	// Get the secret value
	retrievedValue, err := vault.GetSecret(secretName)
	require.NoError(t, err)
	assert.Equal(t, secretValue, retrievedValue)
}

func TestVaultAddSecretMultipleVersions(t *testing.T) {
	fs := afero.NewMemMapFs()
	stateDir := "/test/state"

	// Create vault with long-term key
	vault := createTestVaultWithKey(t, fs, stateDir, "test")

	secretName := "test/secret"

	// Add first version
	err := vault.AddSecret(secretName, []byte("version-1"), false)
	require.NoError(t, err)

	// Try to add again without force - should fail
	err = vault.AddSecret(secretName, []byte("version-2"), false)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "already exists")

	// Add with force - should create new version
	err = vault.AddSecret(secretName, []byte("version-2"), true)
	require.NoError(t, err)

	// Check that we have two versions
	vaultDir, _ := vault.GetDirectory()
	versionsDir := vaultDir + "/secrets.d/test%secret/versions"
	entries, err := afero.ReadDir(fs, versionsDir)
	require.NoError(t, err)
	assert.Len(t, entries, 2)

	// Current value should be version-2
	value, err := vault.GetSecret(secretName)
	require.NoError(t, err)
	assert.Equal(t, []byte("version-2"), value)
}

func TestVaultGetSecretVersion(t *testing.T) {
	fs := afero.NewMemMapFs()
	stateDir := "/test/state"

	// Create vault with long-term key
	vault := createTestVaultWithKey(t, fs, stateDir, "test")

	secretName := "test/secret"

	// Add multiple versions
	err := vault.AddSecret(secretName, []byte("version-1"), false)
	require.NoError(t, err)

	// Small delay to ensure different version names
	time.Sleep(10 * time.Millisecond)

	err = vault.AddSecret(secretName, []byte("version-2"), true)
	require.NoError(t, err)

	// Get versions list
	vaultDir, _ := vault.GetDirectory()
	secretDir := vaultDir + "/secrets.d/test%secret"
	versions, err := secret.ListVersions(fs, secretDir)
	require.NoError(t, err)
	require.Len(t, versions, 2)

	// Get specific version (first one)
	firstVersion := versions[1] // Last in list is first created
	value, err := vault.GetSecretVersion(secretName, firstVersion)
	require.NoError(t, err)
	assert.Equal(t, []byte("version-1"), value)

	// Get specific version (second one)
	secondVersion := versions[0] // First in list is most recent
	value, err = vault.GetSecretVersion(secretName, secondVersion)
	require.NoError(t, err)
	assert.Equal(t, []byte("version-2"), value)

	// Get current (empty version)
	value, err = vault.GetSecretVersion(secretName, "")
	require.NoError(t, err)
	assert.Equal(t, []byte("version-2"), value)
}

func TestVaultVersionTimestamps(t *testing.T) {
	fs := afero.NewMemMapFs()
	stateDir := "/test/state"

	// Create vault with long-term key
	vault := createTestVaultWithKey(t, fs, stateDir, "test")

	// Get long-term key
	ltIdentity, err := vault.GetOrDeriveLongTermKey()
	require.NoError(t, err)

	secretName := "test/secret"

	// Add first version
	beforeFirst := time.Now()
	err = vault.AddSecret(secretName, []byte("version-1"), false)
	require.NoError(t, err)
	afterFirst := time.Now()

	// Get first version metadata
	vaultDir, _ := vault.GetDirectory()
	secretDir := vaultDir + "/secrets.d/test%secret"
	versions, err := secret.ListVersions(fs, secretDir)
	require.NoError(t, err)
	require.Len(t, versions, 1)

	firstVersion := secret.NewSecretVersion(vault, secretName, versions[0])
	err = firstVersion.LoadMetadata(ltIdentity)
	require.NoError(t, err)

	// Check first version timestamps
	assert.NotNil(t, firstVersion.Metadata.CreatedAt)
	assert.True(t, firstVersion.Metadata.CreatedAt.After(beforeFirst.Add(-time.Second)))
	assert.True(t, firstVersion.Metadata.CreatedAt.Before(afterFirst.Add(time.Second)))

	assert.NotNil(t, firstVersion.Metadata.NotBefore)
	assert.Equal(t, int64(1), firstVersion.Metadata.NotBefore.Unix()) // Epoch + 1
	assert.Nil(t, firstVersion.Metadata.NotAfter)                     // Still current

	// Add second version
	time.Sleep(10 * time.Millisecond)
	beforeSecond := time.Now()
	err = vault.AddSecret(secretName, []byte("version-2"), true)
	require.NoError(t, err)
	afterSecond := time.Now()

	// Get updated versions
	versions, err = secret.ListVersions(fs, secretDir)
	require.NoError(t, err)
	require.Len(t, versions, 2)

	// Reload first version metadata (should have notAfter now)
	firstVersion = secret.NewSecretVersion(vault, secretName, versions[1])
	err = firstVersion.LoadMetadata(ltIdentity)
	require.NoError(t, err)

	assert.NotNil(t, firstVersion.Metadata.NotAfter)
	assert.True(t, firstVersion.Metadata.NotAfter.After(beforeSecond.Add(-time.Second)))
	assert.True(t, firstVersion.Metadata.NotAfter.Before(afterSecond.Add(time.Second)))

	// Check second version timestamps
	secondVersion := secret.NewSecretVersion(vault, secretName, versions[0])
	err = secondVersion.LoadMetadata(ltIdentity)
	require.NoError(t, err)

	assert.NotNil(t, secondVersion.Metadata.NotBefore)
	assert.True(t, secondVersion.Metadata.NotBefore.After(beforeSecond.Add(-time.Second)))
	assert.True(t, secondVersion.Metadata.NotBefore.Before(afterSecond.Add(time.Second)))
	assert.Nil(t, secondVersion.Metadata.NotAfter) // Current version
}

func TestVaultGetNonExistentVersion(t *testing.T) {
	fs := afero.NewMemMapFs()
	stateDir := "/test/state"

	// Create vault with long-term key
	vault := createTestVaultWithKey(t, fs, stateDir, "test")

	// Add a secret
	err := vault.AddSecret("test/secret", []byte("value"), false)
	require.NoError(t, err)

	// Try to get non-existent version
	_, err = vault.GetSecretVersion("test/secret", "20991231.999")
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "not found")
}

func TestUpdateVersionMetadata(t *testing.T) {
	fs := afero.NewMemMapFs()
	stateDir := "/test/state"

	// Create vault with long-term key
	vault := createTestVaultWithKey(t, fs, stateDir, "test")

	// Get long-term key
	ltIdentity, err := vault.GetOrDeriveLongTermKey()
	require.NoError(t, err)

	// Create a version manually to test updateVersionMetadata
	secretName := "test/secret"
	versionName := "20231215.001"
	version := secret.NewSecretVersion(vault, secretName, versionName)

	// Set initial metadata
	now := time.Now()
	epochPlusOne := time.Unix(1, 0)
	version.Metadata.NotBefore = &epochPlusOne
	version.Metadata.NotAfter = nil

	// Save version
	err = version.Save([]byte("test-value"))
	require.NoError(t, err)

	// Update metadata
	version.Metadata.NotAfter = &now
	err = updateVersionMetadata(fs, version, ltIdentity)
	require.NoError(t, err)

	// Load and verify
	version2 := secret.NewSecretVersion(vault, secretName, versionName)
	err = version2.LoadMetadata(ltIdentity)
	require.NoError(t, err)

	assert.NotNil(t, version2.Metadata.NotAfter)
	assert.Equal(t, now.Unix(), version2.Metadata.NotAfter.Unix())
}