package minisign

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

// Unencrypted key generated by `minisign -G -W`.
const (
	testUnencryptedSK = `untrusted comment: minisign encrypted secret key
RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOItWpGuGQbG4C9WXaxEYLgZ2xxuqfbuZmDgAhQ8Unot8t7SyxZ0nVh0gESesJ6Ay57fGFJ9T1ajVmanT7MFMCCDbPZ8uqDcSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
`
	testUnencryptedPK = `untrusted comment: minisign public key B141866BA4568B38
RWQ4i1aka4ZBsR0gESesJ6Ay57fGFJ9T1ajVmanT7MFMCCDbPZ8uqDcS
`
)

// Encrypted key generated by `minisign -G` with passphrase "testpass".
const (
	testEncryptedSK = `untrusted comment: minisign encrypted secret key
RWRTY0IyxYlIT2FS5i8PqThE9swBemvY94JDIMqo75UBK3XO/aUAAAACAAAAAAAAAEAAAAAAUhlw8nsT1tuVUekS6Je3iUwoWFdb1xiLonO35G66RiVvM/QgrBtnDa0Dhbt7H3oYMh4aFLiNxMs24gzXqHVsvRVthMeF08fN8r6siRdBpiBZ36B7rox2lmYIYgg5T8qt7tOxo9doAxk=
`
	testEncryptedPK = `untrusted comment: minisign public key 9149E58DCF22FFC1
RWTB/yLPjeVJkXKtzk1nZI0TU+fZPqEaIzg1ABHwfnI8pZNWtifIpWBq
`
	testEncryptedPassphrase = "testpass"
)

func TestDecodePrivateKeyUnencrypted(t *testing.T) {
	sk, err := DecodePrivateKey(testUnencryptedSK)
	if err != nil {
		t.Fatal(err)
	}
	if sk.IsEncrypted() {
		t.Fatal("expected unencrypted key")
	}
	pk, err := DecodePublicKey(testUnencryptedPK)
	if err != nil {
		t.Fatal(err)
	}
	if sk.PublicKey().PublicKey != pk.PublicKey {
		t.Fatal("derived public key does not match")
	}
	if sk.PublicKey().KeyId != pk.KeyId {
		t.Fatal("key ids do not match")
	}
}

func TestDecryptPrivateKey(t *testing.T) {
	sk, err := DecodePrivateKey(testEncryptedSK)
	if err != nil {
		t.Fatal(err)
	}
	if !sk.IsEncrypted() {
		t.Fatal("expected encrypted key")
	}
	if err := sk.Decrypt(testEncryptedPassphrase); err != nil {
		t.Fatal(err)
	}
	if sk.IsEncrypted() {
		t.Fatal("key still flagged as encrypted")
	}
	pk, err := DecodePublicKey(testEncryptedPK)
	if err != nil {
		t.Fatal(err)
	}
	if sk.PublicKey().PublicKey != pk.PublicKey {
		t.Fatal("derived public key does not match")
	}
}

func TestDecryptWrongPassword(t *testing.T) {
	sk, err := DecodePrivateKey(testEncryptedSK)
	if err != nil {
		t.Fatal(err)
	}
	if err := sk.Decrypt("nope"); err == nil {
		t.Fatal("expected error for wrong password")
	}
}

func TestSignRoundTripHashed(t *testing.T) {
	signRoundTrip(t, true)
}

func TestSignRoundTripLegacy(t *testing.T) {
	signRoundTrip(t, false)
}

func signRoundTrip(t *testing.T, hashed bool) {
	t.Helper()
	sk, err := DecodePrivateKey(testUnencryptedSK)
	if err != nil {
		t.Fatal(err)
	}
	pk, err := DecodePublicKey(testUnencryptedPK)
	if err != nil {
		t.Fatal(err)
	}
	data := []byte("the quick brown fox jumps over the lazy dog")
	sig, err := sk.Sign(data, SignOptions{
		UntrustedComment: "hello world",
		TrustedComment:   "file:fox.txt",
		Hashed:           hashed,
	})
	if err != nil {
		t.Fatal(err)
	}

	parsed, err := DecodeSignature(string(sig.Encode()))
	if err != nil {
		t.Fatalf("DecodeSignature: %v", err)
	}
	ok, err := pk.Verify(data, parsed)
	if err != nil {
		t.Fatal(err)
	}
	if !ok {
		t.Fatal("verification failed")
	}
	if parsed.UntrustedComment != "untrusted comment: hello world" {
		t.Fatalf("unexpected untrusted comment: %q", parsed.UntrustedComment)
	}
	if parsed.TrustedComment != "trusted comment: file:fox.txt" {
		t.Fatalf("unexpected trusted comment: %q", parsed.TrustedComment)
	}
}

func TestSignDefaults(t *testing.T) {
	sk, err := DecodePrivateKey(testUnencryptedSK)
	if err != nil {
		t.Fatal(err)
	}
	sig, err := sk.Sign([]byte("hi"), SignOptions{})
	if err != nil {
		t.Fatal(err)
	}
	if !strings.HasPrefix(sig.UntrustedComment, "untrusted comment: signature from minisign secret key") {
		t.Fatalf("unexpected default untrusted comment: %q", sig.UntrustedComment)
	}
	if !strings.HasPrefix(sig.TrustedComment, "trusted comment: timestamp:") {
		t.Fatalf("unexpected default trusted comment: %q", sig.TrustedComment)
	}
}

func TestSignRejectsMultilineComments(t *testing.T) {
	sk, err := DecodePrivateKey(testUnencryptedSK)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := sk.Sign([]byte("x"), SignOptions{UntrustedComment: "a\nb"}); err == nil {
		t.Fatal("expected error for multiline untrusted comment")
	}
	if _, err := sk.Sign([]byte("x"), SignOptions{TrustedComment: "a\nb"}); err == nil {
		t.Fatal("expected error for multiline trusted comment")
	}
}

func TestSignRejectsUnprintableTrustedComment(t *testing.T) {
	sk, err := DecodePrivateKey(testUnencryptedSK)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := sk.Sign([]byte("x"), SignOptions{TrustedComment: "foo\x01bar"}); err == nil {
		t.Fatal("expected error for unprintable trusted comment")
	}
}

func TestSignRejectsOverlongTrustedComment(t *testing.T) {
	sk, err := DecodePrivateKey(testUnencryptedSK)
	if err != nil {
		t.Fatal(err)
	}
	long := strings.Repeat("a", 8200)
	if _, err := sk.Sign([]byte("x"), SignOptions{TrustedComment: long}); err == nil {
		t.Fatal("expected error for overlong trusted comment")
	}
}

func TestSignFileStreamHashedInterop(t *testing.T) {
	bin, err := exec.LookPath("minisign")
	if err != nil {
		t.Skip("minisign binary not available")
	}
	sk, err := DecodePrivateKey(testUnencryptedSK)
	if err != nil {
		t.Fatal(err)
	}
	dir := t.TempDir()
	msgPath := filepath.Join(dir, "big.bin")
	if err := os.WriteFile(msgPath, bytes.Repeat([]byte("the quick brown fox\n"), 4096), 0o644); err != nil {
		t.Fatal(err)
	}
	pubPath := filepath.Join(dir, "pk.pub")
	if err := os.WriteFile(pubPath, []byte(testUnencryptedPK), 0o644); err != nil {
		t.Fatal(err)
	}
	sig, err := sk.SignFile(msgPath, SignOptions{Hashed: true})
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(sig.TrustedComment, "file:big.bin") {
		t.Fatalf("expected default trusted comment to include file:big.bin, got %q", sig.TrustedComment)
	}
	if !strings.Contains(sig.TrustedComment, "hashed") {
		t.Fatalf("expected default trusted comment to include hashed marker, got %q", sig.TrustedComment)
	}
	if err := os.WriteFile(msgPath+".minisig", sig.Encode(), 0o644); err != nil {
		t.Fatal(err)
	}
	out, err := exec.Command(bin, "-Vm", msgPath, "-p", pubPath).CombinedOutput()
	if err != nil {
		t.Fatalf("minisign verify failed: %v\n%s", err, out)
	}
}

func TestSignRequiresDecryptedKey(t *testing.T) {
	sk, err := DecodePrivateKey(testEncryptedSK)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := sk.Sign([]byte("x"), SignOptions{}); err == nil {
		t.Fatal("expected error signing with encrypted key")
	}
}

// TestInteropMinisignCLI signs data with go-minisign and verifies the result
// using the real minisign(1) binary, exercising both legacy and prehashed
// modes. The test is skipped when the binary is unavailable.
func TestInteropMinisignCLI(t *testing.T) {
	bin, err := exec.LookPath("minisign")
	if err != nil {
		t.Skip("minisign binary not available")
	}

	sk, err := DecodePrivateKey(testUnencryptedSK)
	if err != nil {
		t.Fatal(err)
	}

	dir := t.TempDir()
	pubPath := filepath.Join(dir, "test.pub")
	if err := os.WriteFile(pubPath, []byte(testUnencryptedPK), 0o644); err != nil {
		t.Fatal(err)
	}

	for _, hashed := range []bool{true, false} {
		name := "legacy"
		if hashed {
			name = "hashed"
		}
		t.Run(name, func(t *testing.T) {
			msgPath := filepath.Join(dir, "msg-"+name+".txt")
			sigPath := msgPath + ".minisig"
			msg := []byte("minisign interop test " + name)
			if err := os.WriteFile(msgPath, msg, 0o644); err != nil {
				t.Fatal(err)
			}
			sig, err := sk.Sign(msg, SignOptions{
				TrustedComment:   "interop test " + name,
				UntrustedComment: "produced by go-minisign",
				Hashed:           hashed,
			})
			if err != nil {
				t.Fatal(err)
			}
			if err := os.WriteFile(sigPath, sig.Encode(), 0o644); err != nil {
				t.Fatal(err)
			}
			out, err := exec.Command(bin, "-Vm", msgPath, "-p", pubPath).CombinedOutput()
			if err != nil {
				t.Fatalf("minisign verify failed: %v\n%s", err, out)
			}
		})
	}
}

// TestReverseInteropFromCLI signs with the minisign CLI and verifies through
// go-minisign so that signature parsing tolerates the canonical formatting.
func TestReverseInteropFromCLI(t *testing.T) {
	bin, err := exec.LookPath("minisign")
	if err != nil {
		t.Skip("minisign binary not available")
	}
	dir := t.TempDir()
	skPath := filepath.Join(dir, "sk.key")
	pkPath := filepath.Join(dir, "pk.pub")
	if err := os.WriteFile(skPath, []byte(testUnencryptedSK), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(pkPath, []byte(testUnencryptedPK), 0o644); err != nil {
		t.Fatal(err)
	}
	msgPath := filepath.Join(dir, "msg.txt")
	msg := []byte("signed with the cli")
	if err := os.WriteFile(msgPath, msg, 0o644); err != nil {
		t.Fatal(err)
	}
	cmd := exec.Command(bin, "-S", "-s", skPath, "-m", msgPath, "-t", "from CLI")
	cmd.Stdin = strings.NewReader("\n")
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Fatalf("minisign sign: %v\n%s", err, out)
	}
	sigBytes, err := os.ReadFile(msgPath + ".minisig")
	if err != nil {
		t.Fatal(err)
	}
	sig, err := DecodeSignature(string(sigBytes))
	if err != nil {
		t.Fatal(err)
	}
	pk, err := DecodePublicKey(testUnencryptedPK)
	if err != nil {
		t.Fatal(err)
	}
	ok, err := pk.Verify(msg, sig)
	if err != nil {
		t.Fatal(err)
	}
	if !ok {
		t.Fatal("verification failed")
	}
}
