package yubikey

import (
	"crypto/hmac"
	"crypto/sha1"
	"encoding/hex"
	"strings"
	"testing"

	"gitea.apointless.space/bsncubed/dial/internal/store"
)

// fakeKey simulates a YubiKey slot programmed with a fixed secret by computing
// HMAC-SHA1 over the challenge, mirroring ykman's challenge-response output.
func fakeKey(t *testing.T, secret []byte) func(args ...string) ([]byte, error) {
	t.Helper()
	return func(args ...string) ([]byte, error) {
		// args: otp calculate <slot> <hexchallenge>
		if len(args) < 4 {
			t.Fatalf("unexpected ykman args: %v", args)
		}
		chal, err := hex.DecodeString(args[3])
		if err != nil {
			t.Fatalf("bad challenge hex: %v", err)
		}
		m := hmac.New(sha1.New, secret)
		m.Write(chal)
		return []byte(hex.EncodeToString(m.Sum(nil)) + "\n"), nil
	}
}

func TestEnrollThenVerifySucceeds(t *testing.T) {
	orig := runYkman
	defer func() { runYkman = orig }()
	runYkman = fakeKey(t, []byte("secret-slot-2"))

	cfg, err := Enroll(2)
	if err != nil {
		t.Fatal(err)
	}
	if !cfg.IsEnrolled() {
		t.Fatal("config should be enrolled")
	}
	if err := Verify(cfg, 2); err != nil {
		t.Errorf("verify with same key should succeed: %v", err)
	}
}

func TestVerifyWrongKeyFails(t *testing.T) {
	orig := runYkman
	defer func() { runYkman = orig }()

	runYkman = fakeKey(t, []byte("correct-secret"))
	cfg, err := Enroll(2)
	if err != nil {
		t.Fatal(err)
	}

	// Now a different physical key (different secret) is present.
	runYkman = fakeKey(t, []byte("attacker-secret"))
	if err := Verify(cfg, 2); err != ErrKeyMismatch {
		t.Errorf("expected ErrKeyMismatch, got %v", err)
	}
}

func TestInvalidSlot(t *testing.T) {
	if _, err := Enroll(3); err == nil {
		t.Error("slot 3 should be rejected")
	}
}

func TestRecoveryCodeRoundTrip(t *testing.T) {
	code, h, err := GenerateRecoveryCode()
	if err != nil {
		t.Fatal(err)
	}
	// Format: 6 groups of 5 chars separated by dashes.
	parts := strings.Split(code, "-")
	if len(parts) != recoveryGroups {
		t.Fatalf("expected %d groups, got %d (%q)", recoveryGroups, len(parts), code)
	}
	for _, p := range parts {
		if len(p) != recoveryGroupLen {
			t.Errorf("group %q wrong length", p)
		}
	}
	if !VerifyRecoveryCode(code, h) {
		t.Error("exact code should verify")
	}
	// With separators stripped / lowercased it should still verify.
	if !VerifyRecoveryCode(strings.ToLower(strings.ReplaceAll(code, "-", " ")), h) {
		t.Error("normalised code should verify")
	}
	if VerifyRecoveryCode("WRONG-WRONG-WRONG-WRONG-WRONG-WRONG", h) {
		t.Error("wrong code should not verify")
	}
}

func TestVerifyRecoveryUnsetHash(t *testing.T) {
	if VerifyRecoveryCode("anything", store.RecoveryHash{}) {
		t.Error("empty hash should never verify")
	}
}
