package claim

import (
	"strings"
	"testing"

	"gitea.dooplex.hu/admin/felhom-hub/internal/notify"
	"golang.org/x/crypto/bcrypt"
)

// ── R-295, HUB HALF — ONE NAME PER SECRET, AND THE MAIL NAMES THE PAGE THE MACHINE SHOWS ────────
//
// Two different secrets were both called „Visszaállító kód": the THREE-word code that gives a person
// control of the dashboard, and the TEN-word code that opens the sealed off-site backups. They are
// near-homographs of each other and of „Helyreállítási kód", and the collision cost a real code. The
// box side shipped on 2026-08-10; the hub side was dropped twice and was the last place the retired
// name survived — in the mails, which is the one surface a customer reads BEFORE they see any screen.
//
// The ruling these tests pin:
//   - the three-word dashboard code is „Beállító kód" everywhere;
//   - the ten-word escrow code is „Helyreállítási kód";
//   - „Visszaállító kód" is RETIRED;
//   - one secret in two situations keeps its NAME and changes its SENTENCE;
//   - the mail names the page the machine is ACTUALLY SHOWING.
//
// THIS IS NAMING, NOT FUNCTION — TestReenrollSplit_ChangesTheMailNotTheSecret is the pin that says
// so. On the box side the companion pin is the controller's
// TestResetCode_StillAcceptedOnTheSetupPage (claim_code_naming_test.go).

const retiredName = "Visszaállító kód"

// The three code-bearing mails all name the secret „Beállító kód", and none of them carries the
// retired name. The escrow code's name must not appear in a dashboard-code mail either: naming both
// secrets in one message is how a customer comes to believe they are the same thing.
func TestFormatClaimEmail_OneNamePerSecret(t *testing.T) {
	for _, kind := range []EmailKind{EmailClaim, EmailReset, EmailReenroll} {
		subject, body := notify.FormatClaimEmail(string(kind), "c1", "example.hu", "alma-korte-szilva")
		whole := subject + "\n" + body

		if !strings.Contains(body, "Beállító kód: alma-korte-szilva") {
			t.Errorf("%s: the secret must be labelled „Beállító kód”; body was:\n%s", kind, body)
		}
		if strings.Contains(whole, retiredName) {
			t.Errorf("%s: the retired name „%s” is back (subject or body)", kind, retiredName)
		}
		if strings.Contains(whole, "Helyreállítási kód") {
			t.Errorf("%s: names the ESCROW code in a dashboard-code mail — that is the collision", kind)
		}
	}
}

// The two situations that deliver the SAME secret must not name the SAME page, because the machine
// does not show the same page. A box that still has a password shows a login screen carrying an
// „Elfelejtett jelszó" link; a REBUILT box has no password, renders „A szerver beállítása" and serves
// no login page at all (controller web/claim.go:279 — `reset := s.authEnabled()`).
//
// Sending a re-enrolled customer to „Elfelejtett jelszó" names a route that is not on their screen.
// That was the live defect, and this is the test that would have caught it.
func TestFormatClaimEmail_NamesThePageTheMachineShows(t *testing.T) {
	_, resetBody := notify.FormatClaimEmail(string(EmailReset), "c1", "example.hu", "a-b-c")
	if !strings.Contains(resetBody, `"Elfelejtett jelszó"`) {
		t.Errorf("the forgot-password mail should name the page that IS on that customer's screen:\n%s", resetBody)
	}

	_, reBody := notify.FormatClaimEmail(string(EmailReenroll), "c1", "example.hu", "a-b-c")
	if strings.Contains(reBody, "Elfelejtett jelszó") {
		t.Errorf("a REBUILT box serves no login page, so it has no „Elfelejtett jelszó” link:\n%s", reBody)
	}
	if !strings.Contains(reBody, `"A szerver beállítása"`) {
		t.Errorf("the re-enrol mail must name the page a rebuilt box actually shows:\n%s", reBody)
	}
}

// The re-enrol mail must promise nothing about the customer's apps or backups. A clean-slate
// reinstall is exactly the situation in which such a reassurance could be false, and this project has
// spent four register rows (R-294, R-299, R-302, R-311) removing promises it could not see were still
// true. Guarding the CLAIM rather than one phrasing of it: any sentence that says the backups are
// unaffected would have to say so with one of these stems.
func TestFormatClaimEmail_ReenrollPromisesNothingAboutTheData(t *testing.T) {
	_, body := notify.FormatClaimEmail(string(EmailReenroll), "c1", "example.hu", "a-b-c")
	for _, claimWord := range []string{"mentés", "biztonsági", "alkalmazás", "adataid", "visszaállíthat", "visszaszerezhet"} {
		if strings.Contains(body, claimWord) {
			t.Errorf("the re-enrol mail must not talk about data or backups (found %q):\n%s", claimWord, body)
		}
	}
}

// An unrecognised kind falls through to the first-setup mail. That default is SAFE (a rebuilt box
// really is in a setup state) and it is deliberately asserted, because it is the failure mode of
// adding a kind on the engine side and forgetting the template: nothing panics, nothing errors, and
// the wrong-but-harmless mail goes out. Pinning it means a future reader knows it was chosen.
func TestFormatClaimEmail_UnknownKindFallsBackToSetup(t *testing.T) {
	subject, body := notify.FormatClaimEmail("no-such-kind", "c1", "example.hu", "a-b-c")
	if !strings.Contains(subject, "Elindult a Felhom szervered") {
		t.Errorf("unknown kind should fall back to the first-setup mail, got subject %q", subject)
	}
	if strings.Contains(subject+body, retiredName) {
		t.Errorf("the fallback carries the retired name")
	}
}

// THE ACCEPTANCE PIN. Splitting the mail KIND must not touch the secret: ReissueForReenroll still
// rotates the generation, still stores a bcrypt hash that verifies the emailed code, and still stores
// no plaintext — byte-for-byte the same custody as the reset path it was split out of. A rename that
// quietly broke acceptance would be far worse than the collision it fixes.
func TestReenrollSplit_ChangesTheMailNotTheSecret(t *testing.T) {
	e, st, m := newTestEngine(t)
	cc := cust()
	if _, err := e.EnsureIssued(cc); err != nil {
		t.Fatalf("EnsureIssued: %v", err)
	}
	if err := e.MarkClaimed(cc); err != nil {
		t.Fatalf("MarkClaimed: %v", err)
	}
	before, err := st.GetClaim(cc.CustomerID)
	if err != nil {
		t.Fatalf("GetClaim: %v", err)
	}

	gen, reissued, err := e.ReissueForReenroll(cc)
	if err != nil || !reissued {
		t.Fatalf("ReissueForReenroll: gen=%d reissued=%v err=%v", gen, reissued, err)
	}

	// It is the re-enrol mail, not the reset mail — the whole point of the split.
	last := m.sends[len(m.sends)-1]
	if !strings.HasPrefix(last, string(EmailReenroll)+":") {
		t.Errorf("a re-enrolled box must get the reenroll mail, got %q", last)
	}

	// …and the secret behaves exactly as before: generation advanced, the stored hash verifies the
	// newly emailed code, the previous code no longer verifies, and no plaintext is persisted.
	after, err := st.GetClaim(cc.CustomerID)
	if err != nil {
		t.Fatalf("GetClaim after: %v", err)
	}
	if after.Generation != before.Generation+1 {
		t.Errorf("generation %d → %d, want +1", before.Generation, after.Generation)
	}
	if bcrypt.CompareHashAndPassword([]byte(after.CodeHash), []byte(m.lastCode)) != nil {
		t.Error("the stored hash does not verify the emailed code — acceptance moved")
	}
	if strings.Contains(after.CodeHash, m.lastCode) {
		t.Error("the stored hash contains the plaintext code")
	}
}

// RequestReset — a customer who still has a password — must keep sending the RESET mail. The split
// must not have swept the genuine forgot-password path along with it: that customer IS looking at a
// login page, and „Elfelejtett jelszó" is the right thing to name for them.
func TestRequestReset_StillSendsTheResetMail(t *testing.T) {
	e, _, m := newTestEngine(t)
	cc := cust()
	if _, err := e.EnsureIssued(cc); err != nil {
		t.Fatalf("EnsureIssued: %v", err)
	}
	if err := e.MarkClaimed(cc); err != nil {
		t.Fatalf("MarkClaimed: %v", err)
	}
	if err := e.RequestReset(cc); err != nil {
		t.Fatalf("RequestReset: %v", err)
	}
	last := m.sends[len(m.sends)-1]
	if !strings.HasPrefix(last, string(EmailReset)+":") {
		t.Errorf("a forgot-password request must still send the reset mail, got %q", last)
	}
}
