package escrow

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

func ageAvailable() bool {
	if runtime.GOOS != "linux" {
		return false
	}
	if _, err := exec.LookPath("age"); err == nil {
		return true
	}
	return false
}

func ensureAge(t *testing.T) {
	t.Helper()
	if !ageAvailable() {
		t.Skip("skipping: the `age` CLI + linux required (runs on the demo/build host)")
	}
	if p, err := exec.LookPath("age"); err == nil {
		ageBinary = p
	}
}

func TestIdentity_InputValidation(t *testing.T) {
	ctx := context.Background()
	if _, err := WrapIdentity(ctx, nil, "R"); err == nil {
		t.Error("empty bundle must error")
	}
	if _, err := WrapIdentity(ctx, []byte("x"), ""); err == nil {
		t.Error("empty R must error")
	}
	if _, err := UnwrapIdentity(ctx, nil, "R"); err == nil {
		t.Error("empty blob must error")
	}
}

// Round-trip: a bundle wraps under R and recovers byte-identical (the identity analog of K-escrow).
func TestIdentity_RoundTrip(t *testing.T) {
	ensureAge(t)
	ctx := context.Background()
	const R = "throwaway-correct-horse-battery-staple-words"
	bundle := IdentityBundle{TunnelToken: "eyJhIjoidGVzdCIsInQiOiJ4In0", PBSToken: "felhom@pbs!n100:deadbeefcafe"}

	blob, err := WrapIdentityBundle(ctx, bundle, R)
	if err != nil {
		t.Fatalf("WrapIdentityBundle: %v", err)
	}
	// the blob is opaque ciphertext, not the bundle.
	if bytes.Contains(blob, []byte(bundle.TunnelToken)) || bytes.Contains(blob, []byte(bundle.PBSToken)) {
		t.Fatal("the blob leaks plaintext token bytes — not encrypted")
	}
	got, err := UnwrapIdentityBundle(ctx, blob, R)
	if err != nil {
		t.Fatalf("UnwrapIdentityBundle: %v", err)
	}
	if got != bundle {
		t.Errorf("recovered bundle = %+v, want %+v", got, bundle)
	}
}

// fork-4: the IdentityBundle carries the offsite restic repo password under R, byte-exact and encrypted;
// a wrong R fails closed. (The spike proved a recovered value opens the real repo; this guards the field.)
func TestIdentity_RoundTrip_CarriesResticPassword(t *testing.T) {
	ensureAge(t)
	ctx := context.Background()
	const R = "throwaway-correct-horse-battery-staple-fork4"
	const pw = "deadbeefcafef00d0123456789abcdef0123456789abcdef0123456789abcdef" // 64 hex, synthetic
	bundle := IdentityBundle{TunnelToken: "tt", PBSToken: "pt", ResticRepoPassword: pw}
	blob, err := WrapIdentityBundle(ctx, bundle, R)
	if err != nil {
		t.Fatalf("WrapIdentityBundle: %v", err)
	}
	if bytes.Contains(blob, []byte(pw)) {
		t.Fatal("the blob leaks the restic password plaintext — not encrypted")
	}
	got, err := UnwrapIdentityBundle(ctx, blob, R)
	if err != nil {
		t.Fatalf("UnwrapIdentityBundle: %v", err)
	}
	if got.ResticRepoPassword != pw {
		t.Fatalf("recovered restic password not byte-exact: got %q", got.ResticRepoPassword)
	}
	if got != bundle {
		t.Fatalf("recovered bundle = %+v, want %+v", got, bundle)
	}
	if _, err := UnwrapIdentityBundle(ctx, blob, R+"-WRONG"); err == nil {
		t.Fatal("a wrong recovery code must fail closed (no bundle, no restic password)")
	}
}

// PINNED CROSS-REPO TEST VECTOR (SLICE 3): the same vector is asserted in felhom-controller — if either
// side drifts (trailing newline, encoding, trim behavior), its half of this test fails and auto-confirm
// can never silently mismatch. Convention: sha256 hex over the TRIMMED password string.
func TestHashResticPassword_PinnedVector(t *testing.T) {
	const vector = "cafef00ddeadbeef0123456789abcdef0123456789abcdef0123456789abcdef"
	const want = "dbfc02f987e1ac0c91911d5761267089b1144628745a3343e4d96194e43c08e4"
	if got := HashResticPassword(vector); got != want {
		t.Fatalf("pinned vector drift: got %s want %s", got, want)
	}
	// trim convention: surrounding whitespace/newlines do not change the hash (both sides trim)
	if got := HashResticPassword("  " + vector + "\n"); got != want {
		t.Fatalf("whitespace must not change the hash (trim convention), got %s", got)
	}
}

// AttachResticPassword: missing file → clean no-attach; staged file → trimmed value attached; empty → error.
func TestAttachResticPassword(t *testing.T) {
	b := &IdentityBundle{}
	if ok, err := AttachResticPassword(b, filepath.Join(t.TempDir(), "absent")); ok || err != nil {
		t.Fatalf("missing staged file must be a clean no-attach, got ok=%v err=%v", ok, err)
	}
	f := filepath.Join(t.TempDir(), "pw")
	if err := os.WriteFile(f, []byte("  abc123def  \n"), 0o600); err != nil {
		t.Fatal(err)
	}
	ok, err := AttachResticPassword(b, f)
	if err != nil || !ok {
		t.Fatalf("attach from staged file: ok=%v err=%v", ok, err)
	}
	if b.ResticRepoPassword != "abc123def" {
		t.Fatalf("want trimmed value, got %q", b.ResticRepoPassword)
	}
	if err := os.WriteFile(f, []byte("   \n"), 0o600); err != nil {
		t.Fatal(err)
	}
	if _, err := AttachResticPassword(&IdentityBundle{}, f); err == nil {
		t.Fatal("an empty staged file must error (an operator would want to know)")
	}
}

// Wrong R fails CLOSED — no bundle emitted.
func TestIdentity_WrongRFailsClosed(t *testing.T) {
	ensureAge(t)
	ctx := context.Background()
	blob, err := WrapIdentity(ctx, []byte(`{"tunnel_token":"a","pbs_token":"b"}`), "the-correct-code")
	if err != nil {
		t.Fatalf("WrapIdentity: %v", err)
	}
	if _, err := UnwrapIdentity(ctx, blob, "DEFINITELY-the-wrong-code"); err == nil {
		t.Fatal("a wrong recovery code must fail closed (no bundle)")
	}
	// the blob is unchanged / retryable: the RIGHT code still works after a wrong attempt.
	if _, err := UnwrapIdentity(ctx, blob, "the-correct-code"); err != nil {
		t.Errorf("the blob was not retryable after a wrong-R attempt: %v", err)
	}
}
