package escrow

import (
	"context"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
)

func TestWordlistLoaded(t *testing.T) {
	if WordlistSize() != 7776 {
		t.Fatalf("EFF large wordlist should be 7776 words, got %d", WordlistSize())
	}
}

func TestGenerateRecoveryCode_EntropyAndFormat(t *testing.T) {
	if RecoveryCodeEntropyBits() < 128 {
		t.Fatalf("recovery code entropy must be ≥128 bits, got %.1f", RecoveryCodeEntropyBits())
	}
	inList := make(map[string]bool, WordlistSize())
	for _, w := range wordlist {
		inList[w] = true
	}
	for i := 0; i < 50; i++ {
		r, err := GenerateRecoveryCode()
		if err != nil {
			t.Fatalf("GenerateRecoveryCode: %v", err)
		}
		words := strings.Split(r, "-")
		if len(words) != RecoveryCodeWords {
			t.Fatalf("recovery code must be %d words, got %d (%q)", RecoveryCodeWords, len(words), r)
		}
		for _, w := range words {
			if !inList[w] {
				t.Errorf("recovery-code word %q is not from the EFF wordlist", w)
			}
		}
	}
}

func TestGenerateRecoveryCode_Unique(t *testing.T) {
	seen := make(map[string]bool)
	for i := 0; i < 200; i++ {
		r, err := GenerateRecoveryCode()
		if err != nil {
			t.Fatal(err)
		}
		if seen[r] {
			t.Fatalf("recovery code collision within 200 draws — entropy too low")
		}
		seen[r] = true
	}
}

// --- integration: real PBS key wrap/unwrap round-trip (linux + proxmox-backup-client only) ---

func pbcAvailable() bool {
	if runtime.GOOS != "linux" {
		return false
	}
	_, err := exec.LookPath(strings.TrimPrefix(pbcBinary, ""))
	if err != nil {
		// fall back to PATH lookup of the basename
		_, err = exec.LookPath("proxmox-backup-client")
	}
	return err == nil
}

func TestWrapUnwrapRoundTrip(t *testing.T) {
	if !pbcAvailable() {
		t.Skip("skipping: proxmox-backup-client + linux required (runs on the demo/build host)")
	}
	if _, err := os.Stat(pbcBinary); err != nil {
		if p, e := exec.LookPath("proxmox-backup-client"); e == nil {
			pbcBinary = p
		}
	}
	ctx := context.Background()
	dir := t.TempDir()
	Kt := filepath.Join(dir, "Kt.json")
	// throwaway unencrypted key (mimics the live K posture). NOT a real K.
	if out, err := exec.Command(pbcBinary, "key", "create", Kt, "--kdf", "none").CombinedOutput(); err != nil {
		t.Fatalf("key create: %v: %s", err, out)
	}
	ktBefore, _ := os.ReadFile(Kt)
	fp0, err := KeyFingerprint(ctx, Kt)
	if err != nil {
		t.Fatalf("fingerprint: %v", err)
	}

	const Rt = "throwaway-test-passphrase-correct-horse"
	blob := filepath.Join(dir, "escrow.blob")
	if err := Wrap(ctx, Kt, blob, Rt); err != nil {
		t.Fatalf("Wrap: %v", err)
	}
	// the live key file must be byte-unchanged after wrap.
	if ktAfter, _ := os.ReadFile(Kt); string(ktAfter) != string(ktBefore) {
		t.Fatal("the live key file was modified by Wrap — must operate on a copy")
	}
	// blob is the passphrase-protected (scrypt) form.
	if kdf := keyKDF(t, blob); kdf != "scrypt" {
		t.Fatalf("wrapped blob kdf = %q, want scrypt", kdf)
	}

	// unwrap with the RIGHT passphrase → recovered key fingerprint matches the original.
	rec := filepath.Join(dir, "rec.blob")
	if err := copyFile(blob, rec, 0o600); err != nil {
		t.Fatal(err)
	}
	if err := Unwrap(ctx, rec, Rt); err != nil {
		t.Fatalf("Unwrap (correct R): %v", err)
	}
	fp2, err := KeyFingerprint(ctx, rec)
	if err != nil {
		t.Fatal(err)
	}
	if fp2 != fp0 {
		t.Fatalf("recovered key fingerprint %q != original %q", fp2, fp0)
	}

	// unwrap with the WRONG passphrase → must fail (and not produce the key).
	wrong := filepath.Join(dir, "wrong.blob")
	if err := copyFile(blob, wrong, 0o600); err != nil {
		t.Fatal(err)
	}
	if err := Unwrap(ctx, wrong, "definitely-the-wrong-code"); err == nil {
		t.Fatal("Unwrap with the WRONG recovery code must FAIL")
	}
}

func TestCreate_SelfVerifiesAndKeepsKey(t *testing.T) {
	if !pbcAvailable() {
		t.Skip("skipping: proxmox-backup-client + linux required")
	}
	if _, err := os.Stat(pbcBinary); err != nil {
		if p, e := exec.LookPath("proxmox-backup-client"); e == nil {
			pbcBinary = p
		}
	}
	ctx := context.Background()
	dir := t.TempDir()
	Kt := filepath.Join(dir, "Kt.json")
	if out, err := exec.Command(pbcBinary, "key", "create", Kt, "--kdf", "none").CombinedOutput(); err != nil {
		t.Fatalf("key create: %v: %s", err, out)
	}
	ktBefore, _ := os.ReadFile(Kt)

	R, res, err := Create(ctx, CreateOptions{KeyPath: Kt, Posture: PostureZeroKnowledge})
	if err != nil {
		t.Fatalf("Create: %v", err)
	}
	if len(R) == 0 || len(strings.Split(R, "-")) != RecoveryCodeWords {
		t.Errorf("Create returned a malformed recovery code")
	}
	if len(res.Blob) == 0 {
		t.Error("Create returned an empty blob")
	}
	if res.KeyFingerprint == "" || res.Posture != PostureZeroKnowledge {
		t.Errorf("result meta wrong: %+v", res)
	}
	// the blob is ciphertext, not the key: it must NOT equal the live key bytes.
	if string(res.Blob) == string(ktBefore) {
		t.Fatal("blob equals the plaintext key — wrap did not encrypt")
	}
	// the live key file is byte-unchanged.
	if ktAfter, _ := os.ReadFile(Kt); string(ktAfter) != string(ktBefore) {
		t.Fatal("Create modified the live key file")
	}
}

func keyKDF(t *testing.T, path string) string {
	t.Helper()
	out, err := exec.Command(pbcBinary, "key", "show", path, "--output-format", "json").Output()
	if err != nil {
		t.Fatalf("key show: %v", err)
	}
	// minimal parse to avoid importing json twice; the field is `"kdf":"scrypt"` or `"none"`.
	s := string(out)
	for _, k := range []string{"scrypt", "none", "pbkdf2"} {
		if strings.Contains(s, `"kdf":"`+k+`"`) || strings.Contains(s, `"kdf": "`+k+`"`) {
			return k
		}
	}
	return "?(" + s + ")"
}
