package secret_test

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"filippo.io/age"
	"git.eeqj.de/sneak/secret/internal/secret"
	"git.eeqj.de/sneak/secret/internal/vault"
	"git.eeqj.de/sneak/secret/pkg/agehd"
	"github.com/spf13/afero"
)

// Register vault with secret package for testing
func init() {
	// Register the vault.GetCurrentVault function with the secret package
	secret.RegisterGetCurrentVaultFunc(func(fs afero.Fs, stateDir string) (secret.VaultInterface, error) {
		return vault.GetCurrentVault(fs, stateDir)
	})
}

// setupNonInteractiveGPG creates a custom GPG environment for testing
func setupNonInteractiveGPG(t *testing.T, tempDir, passphrase, gnupgHomeDir string) {
	// Create GPG config file for non-interactive operation
	gpgConfPath := filepath.Join(gnupgHomeDir, "gpg.conf")
	gpgConfContent := `batch
no-tty
pinentry-mode loopback
`
	if err := os.WriteFile(gpgConfPath, []byte(gpgConfContent), 0600); err != nil {
		t.Fatalf("Failed to write GPG config file: %v", err)
	}

	// Create a test-specific GPG implementation
	origEncryptFunc := secret.GPGEncryptFunc
	origDecryptFunc := secret.GPGDecryptFunc

	// Set custom GPG functions for this test
	secret.GPGEncryptFunc = func(data []byte, keyID string) ([]byte, error) {
		cmd := exec.Command("gpg",
			"--homedir", gnupgHomeDir,
			"--batch",
			"--yes",
			"--pinentry-mode", "loopback",
			"--passphrase", passphrase,
			"--trust-model", "always",
			"--armor",
			"--encrypt",
			"-r", keyID)

		var stdout, stderr bytes.Buffer
		cmd.Stdout = &stdout
		cmd.Stderr = &stderr
		cmd.Stdin = bytes.NewReader(data)

		if err := cmd.Run(); err != nil {
			return nil, fmt.Errorf("GPG encryption failed: %w\nStderr: %s", err, stderr.String())
		}

		return stdout.Bytes(), nil
	}

	secret.GPGDecryptFunc = func(encryptedData []byte) ([]byte, error) {
		cmd := exec.Command("gpg",
			"--homedir", gnupgHomeDir,
			"--batch",
			"--yes",
			"--pinentry-mode", "loopback",
			"--passphrase", passphrase,
			"--quiet",
			"--decrypt")

		var stdout, stderr bytes.Buffer
		cmd.Stdout = &stdout
		cmd.Stderr = &stderr
		cmd.Stdin = bytes.NewReader(encryptedData)

		if err := cmd.Run(); err != nil {
			return nil, fmt.Errorf("GPG decryption failed: %w\nStderr: %s", err, stderr.String())
		}

		return stdout.Bytes(), nil
	}

	// Restore original functions after test
	t.Cleanup(func() {
		secret.GPGEncryptFunc = origEncryptFunc
		secret.GPGDecryptFunc = origDecryptFunc
	})
}

// runGPGWithPassphrase executes a GPG command with the specified passphrase
func runGPGWithPassphrase(gnupgHome, passphrase string, args []string, input io.Reader) ([]byte, error) {
	cmdArgs := []string{
		"--homedir=" + gnupgHome,
		"--batch",
		"--yes",
		"--pinentry-mode", "loopback",
		"--passphrase", passphrase,
	}
	cmdArgs = append(cmdArgs, args...)

	cmd := exec.Command("gpg", cmdArgs...)
	cmd.Stdin = input

	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	err := cmd.Run()
	if err != nil {
		return nil, fmt.Errorf("GPG command failed: %w\nStderr: %s", err, stderr.String())
	}

	return stdout.Bytes(), nil
}

func TestPGPUnlockerWithRealFS(t *testing.T) {
	// Check if gpg is available
	if _, err := exec.LookPath("gpg"); err != nil {
		t.Log("GPG not available, PGP unlock key tests may not fully function")
		// Continue anyway to test what we can
	}

	// Create a temporary directory for our tests
	tempDir, err := os.MkdirTemp("", "secret-pgp-test-")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tempDir) // Clean up after test

	// Create a temporary GNUPGHOME
	gnupgHomeDir := filepath.Join(tempDir, "gnupg")
	if err := os.MkdirAll(gnupgHomeDir, 0700); err != nil {
		t.Fatalf("Failed to create GNUPGHOME: %v", err)
	}

	// Save original GNUPGHOME
	origGnupgHome := os.Getenv("GNUPGHOME")

	// Set new GNUPGHOME
	os.Setenv("GNUPGHOME", gnupgHomeDir)

	// Clean up environment after test
	defer func() {
		if origGnupgHome != "" {
			os.Setenv("GNUPGHOME", origGnupgHome)
		} else {
			os.Unsetenv("GNUPGHOME")
		}
	}()

	// Test passphrase for GPG key
	testPassphrase := "test123"

	// Setup non-interactive GPG with custom functions
	setupNonInteractiveGPG(t, tempDir, testPassphrase, gnupgHomeDir)

	// Create GPG batch file for key generation
	batchFile := filepath.Join(tempDir, "gen-key-batch")
	batchContent := `%echo Generating a test key
Key-Type: RSA
Key-Length: 2048
Name-Real: Test User
Name-Email: test@example.com
Expire-Date: 0
Passphrase: ` + testPassphrase + `
%commit
%echo Key generation completed
`
	if err := os.WriteFile(batchFile, []byte(batchContent), 0600); err != nil {
		t.Fatalf("Failed to write batch file: %v", err)
	}

	// Generate GPG key with batch mode
	t.Log("Generating GPG key...")
	_, err = runGPGWithPassphrase(gnupgHomeDir, testPassphrase,
		[]string{"--gen-key", batchFile}, nil)
	if err != nil {
		t.Fatalf("Failed to generate GPG key: %v", err)
	}
	t.Log("GPG key generated successfully")

	// Get the key ID
	output, err := runGPGWithPassphrase(gnupgHomeDir, testPassphrase,
		[]string{"--list-secret-keys", "--with-colons"}, nil)
	if err != nil {
		t.Fatalf("Failed to list GPG keys: %v", err)
	}

	// Parse output to get key ID
	var keyID string
	lines := strings.Split(string(output), "\n")
	for _, line := range lines {
		if strings.HasPrefix(line, "sec:") {
			fields := strings.Split(line, ":")
			if len(fields) >= 5 {
				keyID = fields[4]
				break
			}
		}
	}

	if keyID == "" {
		t.Fatalf("Failed to find GPG key ID in output: %s", output)
	}
	t.Logf("Generated GPG key ID: %s", keyID)

	// Set the GPG_AGENT_INFO to empty to ensure gpg-agent doesn't interfere
	oldAgentInfo := os.Getenv("GPG_AGENT_INFO")
	os.Setenv("GPG_AGENT_INFO", "")
	defer func() {
		if oldAgentInfo != "" {
			os.Setenv("GPG_AGENT_INFO", oldAgentInfo)
		} else {
			os.Unsetenv("GPG_AGENT_INFO")
		}
	}()

	// Use the real filesystem
	fs := afero.NewOsFs()

	// Test data
	testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"

	// Save original environment variable
	oldMnemonic := os.Getenv(secret.EnvMnemonic)
	oldGPGKeyID := os.Getenv(secret.EnvGPGKeyID)

	// Set test environment variables
	os.Setenv(secret.EnvMnemonic, testMnemonic)
	os.Setenv(secret.EnvGPGKeyID, keyID)

	// Clean up after test
	defer func() {
		if oldMnemonic != "" {
			os.Setenv(secret.EnvMnemonic, oldMnemonic)
		} else {
			os.Unsetenv(secret.EnvMnemonic)
		}

		if oldGPGKeyID != "" {
			os.Setenv(secret.EnvGPGKeyID, oldGPGKeyID)
		} else {
			os.Unsetenv(secret.EnvGPGKeyID)
		}
	}()

	// Set up vault structure for testing
	stateDir := tempDir
	vaultName := "test-vault"

	// Test creation of a PGP unlock key through a vault
	t.Run("CreatePGPUnlocker", func(t *testing.T) {
		// Set a limited test timeout to avoid hanging
		timer := time.AfterFunc(30*time.Second, func() {
			t.Fatalf("Test timed out after 30 seconds")
		})
		defer timer.Stop()

		// Create a test vault directory structure
		vlt, err := vault.CreateVault(fs, stateDir, vaultName)
		if err != nil {
			t.Fatalf("Failed to create vault: %v", err)
		}

		// Set the current vault
		err = vault.SelectVault(fs, stateDir, vaultName)
		if err != nil {
			t.Fatalf("Failed to select vault: %v", err)
		}

		// Derive long-term key from mnemonic
		ltIdentity, err := agehd.DeriveIdentity(testMnemonic, 0)
		if err != nil {
			t.Fatalf("Failed to derive long-term key: %v", err)
		}

		// Get the vault directory
		vaultDir, err := vlt.GetDirectory()
		if err != nil {
			t.Fatalf("Failed to get vault directory: %v", err)
		}

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

		// Unlock the vault
		vlt.Unlock(ltIdentity)

		// Create a passphrase unlocker first (to have current unlocker)
		passUnlocker, err := vlt.CreatePassphraseUnlocker("test-passphrase")
		if err != nil {
			t.Fatalf("Failed to create passphrase unlocker: %v", err)
		}

		// Verify passphrase unlocker was created
		if passUnlocker == nil {
			t.Fatal("Passphrase unlocker is nil")
		}

		// Now create a PGP unlock key (this will use our custom GPGEncryptFunc)
		pgpUnlocker, err := secret.CreatePGPUnlocker(fs, stateDir, keyID)
		if err != nil {
			t.Fatalf("Failed to create PGP unlock key: %v", err)
		}

		// Verify the PGP unlock key was created
		if pgpUnlocker == nil {
			t.Fatal("PGP unlock key is nil")
		}

		// Check if the key has the correct type
		if pgpUnlocker.GetType() != "pgp" {
			t.Errorf("Expected PGP unlock key type 'pgp', got '%s'", pgpUnlocker.GetType())
		}

		// Check if the key ID includes the GPG key ID
		if !strings.Contains(pgpUnlocker.GetID(), keyID) {
			t.Errorf("PGP unlock key ID '%s' does not contain GPG key ID '%s'", pgpUnlocker.GetID(), keyID)
		}

		// Check if the key directory exists
		unlockerDir := pgpUnlocker.GetDirectory()
		keyExists, err := afero.DirExists(fs, unlockerDir)
		if err != nil {
			t.Fatalf("Failed to check if PGP key directory exists: %v", err)
		}
		if !keyExists {
			t.Errorf("PGP unlock key directory does not exist: %s", unlockerDir)
		}

		// Check if required files exist
		recipientPath := filepath.Join(unlockerDir, "pub.txt")
		recipientExists, err := afero.Exists(fs, recipientPath)
		if err != nil {
			t.Fatalf("Failed to check if recipient file exists: %v", err)
		}
		if !recipientExists {
			t.Errorf("PGP unlock key recipient file does not exist: %s", recipientPath)
		}

		privKeyPath := filepath.Join(unlockerDir, "priv.age.gpg")
		privKeyExists, err := afero.Exists(fs, privKeyPath)
		if err != nil {
			t.Fatalf("Failed to check if private key file exists: %v", err)
		}
		if !privKeyExists {
			t.Errorf("PGP unlock key private key file does not exist: %s", privKeyPath)
		}

		metadataPath := filepath.Join(unlockerDir, "unlocker-metadata.json")
		metadataExists, err := afero.Exists(fs, metadataPath)
		if err != nil {
			t.Fatalf("Failed to check if metadata file exists: %v", err)
		}
		if !metadataExists {
			t.Errorf("PGP unlock key metadata file does not exist: %s", metadataPath)
		}

		longtermPath := filepath.Join(unlockerDir, "longterm.age")
		longtermExists, err := afero.Exists(fs, longtermPath)
		if err != nil {
			t.Fatalf("Failed to check if longterm key file exists: %v", err)
		}
		if !longtermExists {
			t.Errorf("PGP unlock key longterm key file does not exist: %s", longtermPath)
		}

		// Read and verify metadata
		metadataBytes, err := afero.ReadFile(fs, metadataPath)
		if err != nil {
			t.Fatalf("Failed to read metadata: %v", err)
		}

		var metadata struct {
			ID        string    `json:"id"`
			Type      string    `json:"type"`
			CreatedAt time.Time `json:"created_at"`
			Flags     []string  `json:"flags"`
			GPGKeyID  string    `json:"gpg_key_id"`
		}

		if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
			t.Fatalf("Failed to parse metadata: %v", err)
		}

		if metadata.Type != "pgp" {
			t.Errorf("Expected metadata type 'pgp', got '%s'", metadata.Type)
		}

		if metadata.GPGKeyID != keyID {
			t.Errorf("Expected GPG key ID '%s', got '%s'", keyID, metadata.GPGKeyID)
		}
	})

	// Set up key directory for individual tests
	unlockerDir := filepath.Join(tempDir, "unlocker")
	if err := os.MkdirAll(unlockerDir, secret.DirPerms); err != nil {
		t.Fatalf("Failed to create unlocker directory: %v", err)
	}

	// Set up test metadata
	metadata := secret.UnlockerMetadata{
		Type:      "pgp",
		CreatedAt: time.Now(),
		Flags:     []string{"gpg", "encrypted"},
	}

	// Create a PGP unlocker for the remaining tests
	unlocker := secret.NewPGPUnlocker(fs, unlockerDir, metadata)

	// Test getting GPG key ID
	t.Run("GetGPGKeyID", func(t *testing.T) {
		// Create PGP metadata with GPG key ID
		type PGPUnlockerMetadata struct {
			secret.UnlockerMetadata
			GPGKeyID string `json:"gpg_key_id"`
		}

		pgpMetadata := PGPUnlockerMetadata{
			UnlockerMetadata: metadata,
			GPGKeyID:         keyID,
		}

		// Write metadata file
		metadataPath := filepath.Join(unlockerDir, "unlocker-metadata.json")
		metadataBytes, err := json.MarshalIndent(pgpMetadata, "", "  ")
		if err != nil {
			t.Fatalf("Failed to marshal metadata: %v", err)
		}
		if err := afero.WriteFile(fs, metadataPath, metadataBytes, secret.FilePerms); err != nil {
			t.Fatalf("Failed to write metadata: %v", err)
		}

		// Get GPG key ID
		retrievedKeyID, err := unlocker.GetGPGKeyID()
		if err != nil {
			t.Fatalf("Failed to get GPG key ID: %v", err)
		}

		// Verify key ID
		if retrievedKeyID != keyID {
			t.Errorf("Expected GPG key ID '%s', got '%s'", keyID, retrievedKeyID)
		}
	})

	// Test getting identity from PGP unlocker
	t.Run("GetIdentity", func(t *testing.T) {
		// Generate an age identity for testing
		ageIdentity, err := age.GenerateX25519Identity()
		if err != nil {
			t.Fatalf("Failed to generate age identity: %v", err)
		}

		// Write the recipient
		recipientPath := filepath.Join(unlockerDir, "pub.txt")
		if err := afero.WriteFile(fs, recipientPath, []byte(ageIdentity.Recipient().String()), secret.FilePerms); err != nil {
			t.Fatalf("Failed to write recipient: %v", err)
		}

		// GPG encrypt the private key using our custom encrypt function
		privKeyData := []byte(ageIdentity.String())
		encryptedOutput, err := secret.GPGEncryptFunc(privKeyData, keyID)
		if err != nil {
			t.Fatalf("Failed to encrypt with GPG: %v", err)
		}

		// Write the encrypted data to a file
		encryptedPath := filepath.Join(unlockerDir, "priv.age.gpg")
		if err := afero.WriteFile(fs, encryptedPath, encryptedOutput, secret.FilePerms); err != nil {
			t.Fatalf("Failed to write encrypted private key: %v", err)
		}

		// Now try to get the identity - this will use our custom GPGDecryptFunc
		identity, err := unlocker.GetIdentity()
		if err != nil {
			t.Fatalf("Failed to get identity: %v", err)
		}

		// Verify the identity matches
		expectedPubKey := ageIdentity.Recipient().String()
		actualPubKey := identity.Recipient().String()
		if actualPubKey != expectedPubKey {
			t.Errorf("Expected public key '%s', got '%s'", expectedPubKey, actualPubKey)
		}
	})

	// Test removing the unlocker
	t.Run("RemoveUnlocker", func(t *testing.T) {
		// Ensure unlocker directory exists before removal
		keyExists, err := afero.DirExists(fs, unlockerDir)
		if err != nil {
			t.Fatalf("Failed to check if unlocker directory exists: %v", err)
		}
		if !keyExists {
			t.Fatalf("Unlocker directory does not exist: %s", unlockerDir)
		}

		// Remove unlocker
		err = unlocker.Remove()
		if err != nil {
			t.Fatalf("Failed to remove unlocker: %v", err)
		}

		// Verify directory is gone
		keyExists, err = afero.DirExists(fs, unlockerDir)
		if err != nil {
			t.Fatalf("Failed to check if unlocker directory exists: %v", err)
		}
		if keyExists {
			t.Errorf("Unlocker directory still exists after removal: %s", unlockerDir)
		}
	})
}
