package notify

import (
	"io"
	"log"
	"strings"
	"testing"
)

func discardLogger() *log.Logger { return log.New(io.Discard, "", 0) }

// R-158 / R-167 Scenario G — the two new signals of decision D-c sit on OPPOSITE sides of the
// operator/customer line, and each is tested through the real dispatch path rather than by reading
// the register.
//
// D-c's rule, restated: a customer can free space, delete files or add a drive, so a FILL WARNING is
// theirs. A customer can do nothing about a recovery-unit capture failure, so it is not.

// The operator half. Run under the BREAKING configuration — the customer has the event explicitly
// enabled and has an email address — because that is the only configuration in which a missing
// operatorOnlyEvents entry is visible. This is the v0.78.0 defect, demonstrated rather than argued.
func TestRecoveryUnitCaptureFailed_NeverReachesTheCustomer(t *testing.T) {
	st := opOnlyStore(t)
	if err := st.SaveNotificationPrefs("c1", "customer@example.com",
		[]string{"recovery_unit_capture_failed", "backup_failed"}, 6); err != nil {
		t.Fatalf("SaveNotificationPrefs: %v", err)
	}

	rec := &sentTo{}
	d := opOnlyDispatcher(t, st, rec)
	d.ProcessEvent("c1", "recovery_unit_capture_failed", "error",
		`Recovery unit capture FAILED for "immich" — the app has no fresh local (Tier-1) backup`,
		`{"app":"immich","used_percent":100,"space_known":true}`, "controller")

	for _, to := range rec.to {
		if to == "customer@example.com" {
			t.Fatalf("a customer was emailed the OPERATOR-ONLY recovery_unit_capture_failed (%s) — "+
				"enabled_events must not be able to opt in to a failure they cannot act on", to)
		}
	}

	// R-182 CHANGED WHAT THIS ASSERTS, DELIBERATELY, AND THE OLD ASSERTION IS WORTH KEEPING IN VIEW.
	//
	// Until 2026-08-03 this test required the OPERATOR to be e-mailed here, on the grounds that "the
	// alert is the whole point of R-158". That was right when this event was the only signal, and it
	// is wrong now: measured, nine of these arrived at the hub and two were mailed, because the
	// operator cooldown key carries no app identifier — so as an alarm it told the operator about one
	// app and threw the rest away.
	//
	// The type is now RECORD-ONLY: written down every time, never mailed. R-158's guarantee — the
	// operator learns WHICH app failed and WHY — is not weakened, it MOVED: the per-run digest
	// `backup_run_failures` carries every failed app in one mail, and is pinned by
	// backup_run_digest_test.go. The customer safety claim below is untouched and is the reason this
	// test still exists.
	for _, to := range rec.to {
		if to == "operator@felhom.eu" {
			t.Fatal("the operator was e-mailed a PER-APP capture failure — this type is the record " +
				"now, not the alarm. One mail per failing app on a full disk is a dozen mails, which " +
				"is the volume problem the operator ruled against; the digest is the notification")
		}
	}

	// The RECORD must exist, always. It is what makes the digest trustworthy: if the digest is ever
	// lost, delayed or suppressed, the failures are still individually written down. An absent row is
	// equally consistent with "correctly not mailed" and "the dispatcher never ran" — the positive
	// observable is the row itself (standing rule 3).
	logs, err := st.GetRecentNotifications("c1", 20)
	if err != nil {
		t.Fatalf("GetRecentNotifications: %v", err)
	}
	found := false
	for _, l := range logs {
		if l.EventType == "recovery_unit_capture_failed" && l.Status == "recorded" &&
			strings.Contains(l.ErrorMessage, "record-only") {
			found = true
		}
	}
	if !found {
		t.Fatalf("the per-app failure left no 'recorded' row — a failure that is neither mailed nor "+
			"written down is exactly the 2026-08-03 defect, rebuilt; got %d row(s)", len(logs))
	}
}

// The customer half. The fill warning MUST be delivered, and it must render the controller's own
// Hungarian text — which names the drive and the free space — rather than a generic template or the
// raw English.
func TestDiskFillWarning_ReachesTheCustomerInHungarianWithItsNumbers(t *testing.T) {
	st := opOnlyStore(t)
	if err := st.SaveNotificationPrefs("c1", "customer@example.com", []string{"disk_warning"}, 6); err != nil {
		t.Fatal(err)
	}

	// The exact shape the controller sends from v0.191.0.
	const hun = `A(z) „Fotók" tároló 87%-osan megtelt — 4,2 GB szabad hely maradt. ` +
		`Szabadíts fel helyet (törölj felesleges fájlokat, vagy csatlakoztass új meghajtót), ` +
		`különben a biztonsági mentések hamarosan meghiúsulnak.`

	var bodies []string
	d := NewDispatcher(st, "test-key", "from@felhom.eu", "operator@felhom.eu", true, discardLogger())
	d.sendEmailFn = func(to, subject, body string, headers map[string]string) error {
		if to == "customer@example.com" {
			bodies = append(bodies, subject+"\n"+body)
		}
		return nil
	}
	d.ProcessEvent("c1", "disk_warning", "warning", hun,
		`{"label":"Fotók","used_percent":87,"avail_gb":4.2}`, "controller")

	if len(bodies) == 0 {
		t.Fatal("the customer was NOT warned that a disk is filling — this is the customer half of " +
			"decision D-c, and nothing reached them")
	}
	got := strings.Join(bodies, "\n")

	// The controller's dynamic text must survive. A customerMessages entry for disk_warning would
	// OVERRIDE it (templates.go prefers the entry) and discard the drive name and the free space,
	// leaving the customer with "A lemezterület 90% felett van" and nothing to act on — which is
	// exactly why v0.191.0 removes those two entries.
	if !strings.Contains(got, "Fotók") {
		t.Fatalf("the customer email does not name the drive — a generic customerMessages entry has "+
			"discarded the controller's dynamic text (templates.go priority). Got:\n%s", got)
	}
	if !strings.Contains(got, "4,2 GB") {
		t.Fatalf("the customer email does not carry the free-space figure. Got:\n%s", got)
	}
	if !strings.Contains(got, "Szabadíts fel helyet") {
		t.Fatalf("the customer email does not tell the customer what to DO. Got:\n%s", got)
	}
}

// The generic entries must STAY REMOVED. A well-meaning re-add would silently re-break the test
// above's guarantee for every future reader — this pins the deletion itself.
func TestDiskFillTypesHaveNoGenericCustomerMessage(t *testing.T) {
	for _, et := range []string{"disk_warning", "disk_critical"} {
		if msg, ok := customerMessages[et]; ok {
			t.Fatalf("customerMessages[%q] = %q — a static entry OVERRIDES the controller's dynamic "+
				"Hungarian text (FormatCustomerEmail prefers the entry), discarding the drive name "+
				"and the free-space figure the customer needs. Same reason offbox_enlarge_blocked "+
				"and disk_health_degraded deliberately have none", et, msg)
		}
	}
}
