package notify

import (
	"encoding/json"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"
	"time"

	"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
)

// capturedEvent is one POST /api/v1/event body the hub would have received.
type capturedEvent struct {
	EventType string `json:"event_type"`
	Severity  string `json:"severity"`
	Message   string `json:"message"`
}

// notifierAgainstHub wires a REAL Notifier at an httptest hub and returns a channel of what actually
// went over the wire. The whole failure class E-2b addresses is "the method exists and nobody calls
// it / nothing arrives", so the test has to observe the WIRE, not a mock's call count.
func notifierAgainstHub(t *testing.T) (*Notifier, <-chan capturedEvent) {
	t.Helper()
	got := make(chan capturedEvent, 8)
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		body, _ := io.ReadAll(r.Body)
		var ev capturedEvent
		_ = json.Unmarshal(body, &ev)
		got <- ev
		w.WriteHeader(http.StatusOK)
	}))
	t.Cleanup(srv.Close)

	sett, err := settings.Load(filepath.Join(t.TempDir(), "settings.json"), log.New(io.Discard, "", 0))
	if err != nil {
		t.Fatalf("settings: %v", err)
	}
	n := New(srv.URL, "test-key", "demo-hp", sett, log.New(os.Stderr, "", 0), false)
	return n, got
}

func awaitEvent(t *testing.T, ch <-chan capturedEvent) capturedEvent {
	t.Helper()
	select {
	case ev := <-ch:
		return ev
	case <-time.After(3 * time.Second):
		t.Fatal("no event reached the hub within 3s — the notification never went out")
		return capturedEvent{}
	}
}

// E-2 Part 5 — the absent backup target must emit its OWN event type, at error severity.
//
// It has to be `backup_target_absent` specifically: the hub allowlists that string, and an event type
// the hub does not allowlist is answered 400 and VANISHES (R-97a). A typo here is not a cosmetic bug,
// it is total silence — the exact condition E-2 exists to end.
func TestBackupTargetAbsentEmitsItsOwnEventType(t *testing.T) {
	n, got := notifierAgainstHub(t)
	n.NotifyBackupTargetAbsent("Külső HDD 1TB", "/mnt/hdd_1")
	ev := awaitEvent(t, got)
	if ev.EventType != "backup_target_absent" {
		t.Errorf("event_type = %q, want backup_target_absent (any other string is 400'd by the hub and lost)", ev.EventType)
	}
	if ev.Severity != "error" {
		t.Errorf("severity = %q, want error — a lost whole-system backup is not a warning", ev.Severity)
	}
	if ev.Message == "" {
		t.Error("message is empty — the operator mail would name no drive")
	}
}

// The recovery half. `info` severity is deliberate and load-bearing: the recovery-mail pairing is
// gated on it, and widening severityNotifies to carry recoveries at warning/error would page the
// operator for good news across every event family, not just this one.
func TestBackupTargetRestoredIsPairedAndInfo(t *testing.T) {
	n, got := notifierAgainstHub(t)
	n.NotifyBackupTargetRestored("Külső HDD 1TB", "/mnt/hdd_1")
	ev := awaitEvent(t, got)
	if ev.EventType != "backup_target_restored" {
		t.Errorf("event_type = %q, want backup_target_restored", ev.EventType)
	}
	if ev.Severity != "info" {
		t.Errorf("severity = %q, want info — recovery mails are pairing-gated on info", ev.Severity)
	}
}

// E-2b — the seam that was defined and never called. This pins that NotifyStorageDisconnected
// actually reaches the hub, which it never did before E-2: it was registered in allowedEventTypes,
// DefaultEnabledEvents and the hub's Hungarian customerMessages, and called from nowhere in the repo.
func TestStorageDisconnectedActuallyReachesTheHub(t *testing.T) {
	n, got := notifierAgainstHub(t)
	n.NotifyStorageDisconnected("Külső HDD 1TB", []string{"paperless"})
	ev := awaitEvent(t, got)
	if ev.EventType != "storage_disconnected" {
		t.Errorf("event_type = %q, want storage_disconnected", ev.EventType)
	}
}
