package mfer

import (
	"bytes"
	"context"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

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

// testGPGEnv sets up a temporary GPG home directory with a test key.
// Returns the key ID and a cleanup function.
func testGPGEnv(t *testing.T) (GPGKeyID, func()) {
	t.Helper()

	// Check if gpg is installed
	if _, err := exec.LookPath("gpg"); err != nil {
		t.Skip("gpg not installed, skipping signing test")
		return "", func() {}
	}

	// Create temporary GPG home directory
	gpgHome, err := os.MkdirTemp("", "mfer-gpg-test-*")
	require.NoError(t, err)

	// Set restrictive permissions on GPG home
	require.NoError(t, os.Chmod(gpgHome, 0o700))

	// Save original GNUPGHOME and set new one
	origGPGHome := os.Getenv("GNUPGHOME")
	os.Setenv("GNUPGHOME", gpgHome)

	cleanup := func() {
		if origGPGHome == "" {
			os.Unsetenv("GNUPGHOME")
		} else {
			os.Setenv("GNUPGHOME", origGPGHome)
		}
		os.RemoveAll(gpgHome)
	}

	// Generate a test key with no passphrase
	keyParams := `%no-protection
Key-Type: RSA
Key-Length: 2048
Name-Real: MFER Test Key
Name-Email: test@mfer.test
Expire-Date: 0
%commit
`
	paramsFile := filepath.Join(gpgHome, "key-params")
	require.NoError(t, os.WriteFile(paramsFile, []byte(keyParams), 0o600))

	cmd := exec.Command("gpg", "--batch", "--gen-key", paramsFile)
	cmd.Env = append(os.Environ(), "GNUPGHOME="+gpgHome)
	output, err := cmd.CombinedOutput()
	if err != nil {
		cleanup()
		t.Skipf("failed to generate test GPG key: %v: %s", err, output)
		return "", func() {}
	}

	// Get the key fingerprint
	cmd = exec.Command("gpg", "--list-keys", "--with-colons", "test@mfer.test")
	cmd.Env = append(os.Environ(), "GNUPGHOME="+gpgHome)
	output, err = cmd.Output()
	if err != nil {
		cleanup()
		t.Fatalf("failed to list test key: %v", err)
	}

	// Parse fingerprint from output
	var keyID string
	for _, line := range strings.Split(string(output), "\n") {
		fields := strings.Split(line, ":")
		if len(fields) >= 10 && fields[0] == "fpr" {
			keyID = fields[9]
			break
		}
	}

	if keyID == "" {
		cleanup()
		t.Fatal("failed to find test key fingerprint")
	}

	return GPGKeyID(keyID), cleanup
}

func TestGPGSign(t *testing.T) {
	keyID, cleanup := testGPGEnv(t)
	defer cleanup()

	data := []byte("test data to sign")
	sig, err := gpgSign(data, keyID)
	require.NoError(t, err)
	assert.NotEmpty(t, sig)
	assert.Contains(t, string(sig), "-----BEGIN PGP SIGNATURE-----")
	assert.Contains(t, string(sig), "-----END PGP SIGNATURE-----")
}

func TestGPGExportPublicKey(t *testing.T) {
	keyID, cleanup := testGPGEnv(t)
	defer cleanup()

	pubKey, err := gpgExportPublicKey(keyID)
	require.NoError(t, err)
	assert.NotEmpty(t, pubKey)
	assert.Contains(t, string(pubKey), "-----BEGIN PGP PUBLIC KEY BLOCK-----")
	assert.Contains(t, string(pubKey), "-----END PGP PUBLIC KEY BLOCK-----")
}

func TestGPGGetKeyFingerprint(t *testing.T) {
	keyID, cleanup := testGPGEnv(t)
	defer cleanup()

	fingerprint, err := gpgGetKeyFingerprint(keyID)
	require.NoError(t, err)
	assert.NotEmpty(t, fingerprint)
	// The fingerprint should be 40 hex chars
	assert.Len(t, fingerprint, 40, "fingerprint should be 40 hex chars")
}

func TestGPGSignInvalidKey(t *testing.T) {
	// Set up test environment (we need GNUPGHOME set)
	_, cleanup := testGPGEnv(t)
	defer cleanup()

	data := []byte("test data")
	_, err := gpgSign(data, GPGKeyID("NONEXISTENT_KEY_ID_12345"))
	assert.Error(t, err)
}

func TestBuilderWithSigning(t *testing.T) {
	keyID, cleanup := testGPGEnv(t)
	defer cleanup()

	// Create a builder with signing options
	b := NewBuilder()
	b.SetSigningOptions(&SigningOptions{
		KeyID: keyID,
	})

	// Add a test file
	content := []byte("test file content")
	reader := bytes.NewReader(content)
	_, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, reader, nil)
	require.NoError(t, err)

	// Build the manifest
	var buf bytes.Buffer
	err = b.Build(&buf)
	require.NoError(t, err)

	// Parse the manifest and verify signature fields are populated
	manifest, err := NewManifestFromReader(&buf)
	require.NoError(t, err)
	require.NotNil(t, manifest.pbOuter)

	assert.NotEmpty(t, manifest.pbOuter.Signature, "signature should be populated")
	assert.NotEmpty(t, manifest.pbOuter.Signer, "signer should be populated")
	assert.NotEmpty(t, manifest.pbOuter.SigningPubKey, "signing public key should be populated")

	// Verify signature is a valid PGP signature
	assert.Contains(t, string(manifest.pbOuter.Signature), "-----BEGIN PGP SIGNATURE-----")

	// Verify public key is a valid PGP public key block
	assert.Contains(t, string(manifest.pbOuter.SigningPubKey), "-----BEGIN PGP PUBLIC KEY BLOCK-----")
}

func TestScannerWithSigning(t *testing.T) {
	keyID, cleanup := testGPGEnv(t)
	defer cleanup()

	// Create in-memory filesystem with test files
	fs := afero.NewMemMapFs()
	require.NoError(t, fs.MkdirAll("/testdir", 0o755))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("content1"), 0o644))
	require.NoError(t, afero.WriteFile(fs, "/testdir/file2.txt", []byte("content2"), 0o644))

	// Create scanner with signing options
	opts := &ScannerOptions{
		Fs: fs,
		SigningOptions: &SigningOptions{
			KeyID: keyID,
		},
	}
	s := NewScannerWithOptions(opts)

	// Enumerate files
	require.NoError(t, s.EnumeratePath("/testdir", nil))
	assert.Equal(t, FileCount(2), s.FileCount())

	// Generate signed manifest
	var buf bytes.Buffer
	require.NoError(t, s.ToManifest(context.Background(), &buf, nil))

	// Parse and verify
	manifest, err := NewManifestFromReader(&buf)
	require.NoError(t, err)

	assert.NotEmpty(t, manifest.pbOuter.Signature)
	assert.NotEmpty(t, manifest.pbOuter.Signer)
	assert.NotEmpty(t, manifest.pbOuter.SigningPubKey)
}

func TestGPGVerify(t *testing.T) {
	keyID, cleanup := testGPGEnv(t)
	defer cleanup()

	data := []byte("test data to sign and verify")
	sig, err := gpgSign(data, keyID)
	require.NoError(t, err)

	pubKey, err := gpgExportPublicKey(keyID)
	require.NoError(t, err)

	// Verify the signature
	err = gpgVerify(data, sig, pubKey)
	require.NoError(t, err)
}

func TestGPGVerifyInvalidSignature(t *testing.T) {
	keyID, cleanup := testGPGEnv(t)
	defer cleanup()

	data := []byte("test data to sign")
	sig, err := gpgSign(data, keyID)
	require.NoError(t, err)

	pubKey, err := gpgExportPublicKey(keyID)
	require.NoError(t, err)

	// Try to verify with different data - should fail
	wrongData := []byte("different data")
	err = gpgVerify(wrongData, sig, pubKey)
	assert.Error(t, err)
}

func TestGPGVerifyBadPublicKey(t *testing.T) {
	keyID, cleanup := testGPGEnv(t)
	defer cleanup()

	data := []byte("test data")
	sig, err := gpgSign(data, keyID)
	require.NoError(t, err)

	// Try to verify with invalid public key - should fail
	badPubKey := []byte("not a valid public key")
	err = gpgVerify(data, sig, badPubKey)
	assert.Error(t, err)
}

func TestManifestSignatureVerification(t *testing.T) {
	keyID, cleanup := testGPGEnv(t)
	defer cleanup()

	// Create a builder with signing options
	b := NewBuilder()
	b.SetSigningOptions(&SigningOptions{
		KeyID: keyID,
	})

	// Add a test file
	content := []byte("test file content for verification")
	reader := bytes.NewReader(content)
	_, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, reader, nil)
	require.NoError(t, err)

	// Build the manifest
	var buf bytes.Buffer
	err = b.Build(&buf)
	require.NoError(t, err)

	// Parse the manifest - signature should be verified during load
	manifest, err := NewManifestFromReader(&buf)
	require.NoError(t, err)
	require.NotNil(t, manifest)

	// Signature should be present and valid
	assert.NotEmpty(t, manifest.pbOuter.Signature)
}

func TestManifestTamperedSignatureFails(t *testing.T) {
	keyID, cleanup := testGPGEnv(t)
	defer cleanup()

	// Create a signed manifest
	b := NewBuilder()
	b.SetSigningOptions(&SigningOptions{
		KeyID: keyID,
	})

	content := []byte("test file content")
	reader := bytes.NewReader(content)
	_, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, reader, nil)
	require.NoError(t, err)

	var buf bytes.Buffer
	err = b.Build(&buf)
	require.NoError(t, err)

	// Tamper with the signature by replacing some bytes
	data := buf.Bytes()
	// Find and modify a byte in the signature portion
	for i := range data {
		if i > 100 && data[i] == 'A' {
			data[i] = 'B'
			break
		}
	}

	// Try to load the tampered manifest - should fail
	_, err = NewManifestFromReader(bytes.NewReader(data))
	assert.Error(t, err)
}

func TestBuilderWithoutSigning(t *testing.T) {
	// Create a builder without signing options
	b := NewBuilder()

	// Add a test file
	content := []byte("test file content")
	reader := bytes.NewReader(content)
	_, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, reader, nil)
	require.NoError(t, err)

	// Build the manifest
	var buf bytes.Buffer
	err = b.Build(&buf)
	require.NoError(t, err)

	// Parse the manifest and verify signature fields are empty
	manifest, err := NewManifestFromReader(&buf)
	require.NoError(t, err)
	require.NotNil(t, manifest.pbOuter)

	assert.Empty(t, manifest.pbOuter.Signature, "signature should be empty when not signing")
	assert.Empty(t, manifest.pbOuter.Signer, "signer should be empty when not signing")
	assert.Empty(t, manifest.pbOuter.SigningPubKey, "signing public key should be empty when not signing")
}
