// Version Support Test Suite Documentation
//
// This file contains core unit tests for version functionality:
//
// - TestGenerateVersionName: Tests version name generation with date and serial format
// - TestGenerateVersionNameMaxSerial: Tests the 999 versions per day limit
// - TestNewSecretVersion: Tests secret version object creation
// - TestSecretVersionSave: Tests saving a version with encryption
// - TestSecretVersionLoadMetadata: Tests loading and decrypting version metadata
// - TestSecretVersionGetValue: Tests retrieving and decrypting version values
// - TestListVersions: Tests listing versions in reverse chronological order
// - TestGetCurrentVersion: Tests retrieving the current version via symlink
// - TestSetCurrentVersion: Tests updating the current version symlink
// - TestVersionMetadataTimestamps: Tests timestamp pointer consistency
//
// Key Test Scenarios:
// - Version Creation: First version gets notBefore = epoch + 1 second
// - Subsequent versions update previous version's notAfter timestamp
// - New version's notBefore equals previous version's notAfter
// - Version names follow YYYYMMDD.NNN format
// - Maximum 999 versions per day enforced
//
// Version Retrieval:
// - Get current version via symlink
// - Get specific version by name
// - Empty version parameter returns current
// - Non-existent versions return appropriate errors
//
// Data Integrity:
// - Each version has independent encryption keys
// - Metadata encryption protects version history
// - Long-term key required for all operations
// - Concurrent reads handled safely

package secret

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

	"filippo.io/age"
	"github.com/spf13/afero"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// MockVault implements VaultInterface for testing
type MockVersionVault struct {
	Name        string
	fs          afero.Fs
	stateDir    string
	longTermKey *age.X25519Identity
}

func (m *MockVersionVault) GetDirectory() (string, error) {
	return filepath.Join(m.stateDir, "vaults.d", m.Name), nil
}

func (m *MockVersionVault) AddSecret(name string, value []byte, force bool) error {
	return fmt.Errorf("not implemented in mock")
}

func (m *MockVersionVault) GetName() string {
	return m.Name
}

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

func (m *MockVersionVault) GetCurrentUnlocker() (Unlocker, error) {
	return nil, fmt.Errorf("not implemented in mock")
}

func (m *MockVersionVault) CreatePassphraseUnlocker(passphrase string) (*PassphraseUnlocker, error) {
	return nil, fmt.Errorf("not implemented in mock")
}

func TestGenerateVersionName(t *testing.T) {
	fs := afero.NewMemMapFs()
	secretDir := "/test/secret"

	// Test first version generation
	version1, err := GenerateVersionName(fs, secretDir)
	require.NoError(t, err)
	assert.Regexp(t, `^\d{8}\.001$`, version1)

	// Create the version directory
	versionDir := filepath.Join(secretDir, "versions", version1)
	err = fs.MkdirAll(versionDir, 0755)
	require.NoError(t, err)

	// Test second version generation on same day
	version2, err := GenerateVersionName(fs, secretDir)
	require.NoError(t, err)
	assert.Regexp(t, `^\d{8}\.002$`, version2)

	// Verify they have the same date prefix
	assert.Equal(t, version1[:8], version2[:8])
	assert.NotEqual(t, version1, version2)
}

func TestGenerateVersionNameMaxSerial(t *testing.T) {
	fs := afero.NewMemMapFs()
	secretDir := "/test/secret"
	versionsDir := filepath.Join(secretDir, "versions")

	// Create 999 versions
	today := time.Now().Format("20060102")
	for i := 1; i <= 999; i++ {
		versionName := fmt.Sprintf("%s.%03d", today, i)
		err := fs.MkdirAll(filepath.Join(versionsDir, versionName), 0755)
		require.NoError(t, err)
	}

	// Try to create one more - should fail
	_, err := GenerateVersionName(fs, secretDir)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "exceeded maximum versions per day")
}

func TestNewSecretVersion(t *testing.T) {
	fs := afero.NewMemMapFs()
	vault := &MockVersionVault{
		Name:     "test",
		fs:       fs,
		stateDir: "/test",
	}

	sv := NewSecretVersion(vault, "test/secret", "20231215.001")

	assert.Equal(t, "test/secret", sv.SecretName)
	assert.Equal(t, "20231215.001", sv.Version)
	assert.Contains(t, sv.Directory, "test%secret/versions/20231215.001")
	assert.NotEmpty(t, sv.Metadata.ID)
	assert.NotNil(t, sv.Metadata.CreatedAt)
}

func TestSecretVersionSave(t *testing.T) {
	fs := afero.NewMemMapFs()
	vault := &MockVersionVault{
		Name:     "test",
		fs:       fs,
		stateDir: "/test",
	}

	// Create vault directory structure and long-term key
	vaultDir, _ := vault.GetDirectory()
	err := fs.MkdirAll(vaultDir, 0755)
	require.NoError(t, err)

	// Generate and store long-term public key
	ltIdentity, err := age.GenerateX25519Identity()
	require.NoError(t, err)
	vault.longTermKey = ltIdentity

	ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
	err = afero.WriteFile(fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0600)
	require.NoError(t, err)

	// Create and save a version
	sv := NewSecretVersion(vault, "test/secret", "20231215.001")
	testValue := []byte("test-secret-value")

	err = sv.Save(testValue)
	require.NoError(t, err)

	// Verify files were created
	assert.True(t, fileExists(fs, filepath.Join(sv.Directory, "pub.age")))
	assert.True(t, fileExists(fs, filepath.Join(sv.Directory, "priv.age")))
	assert.True(t, fileExists(fs, filepath.Join(sv.Directory, "value.age")))
	assert.True(t, fileExists(fs, filepath.Join(sv.Directory, "metadata.age")))
}

func TestSecretVersionLoadMetadata(t *testing.T) {
	fs := afero.NewMemMapFs()
	vault := &MockVersionVault{
		Name:     "test",
		fs:       fs,
		stateDir: "/test",
	}

	// Setup vault with long-term key
	vaultDir, _ := vault.GetDirectory()
	err := fs.MkdirAll(vaultDir, 0755)
	require.NoError(t, err)

	ltIdentity, err := age.GenerateX25519Identity()
	require.NoError(t, err)
	vault.longTermKey = ltIdentity

	ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
	err = afero.WriteFile(fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0600)
	require.NoError(t, err)

	// Create and save a version with custom metadata
	sv := NewSecretVersion(vault, "test/secret", "20231215.001")
	now := time.Now()
	epochPlusOne := time.Unix(1, 0)
	sv.Metadata.NotBefore = &epochPlusOne
	sv.Metadata.NotAfter = &now

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

	// Create new version object and load metadata
	sv2 := NewSecretVersion(vault, "test/secret", "20231215.001")
	err = sv2.LoadMetadata(ltIdentity)
	require.NoError(t, err)

	// Verify loaded metadata
	assert.Equal(t, sv.Metadata.ID, sv2.Metadata.ID)
	assert.NotNil(t, sv2.Metadata.NotBefore)
	assert.Equal(t, epochPlusOne.Unix(), sv2.Metadata.NotBefore.Unix())
	assert.NotNil(t, sv2.Metadata.NotAfter)
}

func TestSecretVersionGetValue(t *testing.T) {
	fs := afero.NewMemMapFs()
	vault := &MockVersionVault{
		Name:     "test",
		fs:       fs,
		stateDir: "/test",
	}

	// Setup vault with long-term key
	vaultDir, _ := vault.GetDirectory()
	err := fs.MkdirAll(vaultDir, 0755)
	require.NoError(t, err)

	ltIdentity, err := age.GenerateX25519Identity()
	require.NoError(t, err)
	vault.longTermKey = ltIdentity

	ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
	err = afero.WriteFile(fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0600)
	require.NoError(t, err)

	// Create and save a version
	sv := NewSecretVersion(vault, "test/secret", "20231215.001")
	originalValue := []byte("test-secret-value-12345")

	err = sv.Save(originalValue)
	require.NoError(t, err)

	// Retrieve the value
	retrievedValue, err := sv.GetValue(ltIdentity)
	require.NoError(t, err)

	assert.Equal(t, originalValue, retrievedValue)
}

func TestListVersions(t *testing.T) {
	fs := afero.NewMemMapFs()
	secretDir := "/test/secret"
	versionsDir := filepath.Join(secretDir, "versions")

	// No versions directory
	versions, err := ListVersions(fs, secretDir)
	require.NoError(t, err)
	assert.Empty(t, versions)

	// Create some versions
	testVersions := []string{"20231215.001", "20231215.002", "20231216.001", "20231214.001"}
	for _, v := range testVersions {
		err := fs.MkdirAll(filepath.Join(versionsDir, v), 0755)
		require.NoError(t, err)
	}

	// Create a file (not directory) that should be ignored
	err = afero.WriteFile(fs, filepath.Join(versionsDir, "ignore.txt"), []byte("test"), 0600)
	require.NoError(t, err)

	// List versions
	versions, err = ListVersions(fs, secretDir)
	require.NoError(t, err)

	// Should be sorted in reverse chronological order
	expected := []string{"20231216.001", "20231215.002", "20231215.001", "20231214.001"}
	assert.Equal(t, expected, versions)
}

func TestGetCurrentVersion(t *testing.T) {
	fs := afero.NewMemMapFs()
	secretDir := "/test/secret"

	// Simulate symlink with file content (works for both OsFs and MemMapFs)
	currentPath := filepath.Join(secretDir, "current")
	err := fs.MkdirAll(secretDir, 0755)
	require.NoError(t, err)

	err = afero.WriteFile(fs, currentPath, []byte("versions/20231216.001"), 0600)
	require.NoError(t, err)

	version, err := GetCurrentVersion(fs, secretDir)
	require.NoError(t, err)
	assert.Equal(t, "20231216.001", version)
}

func TestSetCurrentVersion(t *testing.T) {
	fs := afero.NewMemMapFs()
	secretDir := "/test/secret"

	err := fs.MkdirAll(secretDir, 0755)
	require.NoError(t, err)

	// Set current version
	err = SetCurrentVersion(fs, secretDir, "20231216.002")
	require.NoError(t, err)

	// Verify it was set
	version, err := GetCurrentVersion(fs, secretDir)
	require.NoError(t, err)
	assert.Equal(t, "20231216.002", version)

	// Update to different version
	err = SetCurrentVersion(fs, secretDir, "20231217.001")
	require.NoError(t, err)

	version, err = GetCurrentVersion(fs, secretDir)
	require.NoError(t, err)
	assert.Equal(t, "20231217.001", version)
}

func TestVersionMetadataTimestamps(t *testing.T) {
	// Test that all timestamp fields behave consistently as pointers
	vm := VersionMetadata{
		ID: "test-id",
	}

	// All should be nil initially
	assert.Nil(t, vm.CreatedAt)
	assert.Nil(t, vm.NotBefore)
	assert.Nil(t, vm.NotAfter)

	// Set timestamps
	now := time.Now()
	epoch := time.Unix(1, 0)
	future := now.Add(time.Hour)

	vm.CreatedAt = &now
	vm.NotBefore = &epoch
	vm.NotAfter = &future

	// All should be non-nil
	assert.NotNil(t, vm.CreatedAt)
	assert.NotNil(t, vm.NotBefore)
	assert.NotNil(t, vm.NotAfter)

	// Values should match
	assert.Equal(t, now.Unix(), vm.CreatedAt.Unix())
	assert.Equal(t, int64(1), vm.NotBefore.Unix())
	assert.Equal(t, future.Unix(), vm.NotAfter.Unix())
}

// Helper function
func fileExists(fs afero.Fs, path string) bool {
	exists, _ := afero.Exists(fs, path)
	return exists
}
