package plugin

import (
	"encoding/json"
	"strings"
	"testing"
	"unicode/utf8"

	"github.com/alexei/tinyforge/internal/events"
	"github.com/alexei/tinyforge/internal/store"
)

// capturePublisher records every event published on it so a test can
// assert on the bus payload. Satisfies plugin.EventPublisher.
type capturePublisher struct {
	events []events.Event
}

func (c *capturePublisher) Publish(evt events.Event) {
	c.events = append(c.events, evt)
}

// newEmitDeps builds a plugin.Deps backed by an in-memory store and a
// capturing publisher. Mirrors the in-memory store pattern used by the
// store + source-plugin tests.
func newEmitDeps(t *testing.T) (Deps, *capturePublisher) {
	t.Helper()
	st, err := store.New(":memory:")
	if err != nil {
		t.Fatalf("open store: %v", err)
	}
	t.Cleanup(func() { _ = st.Close() })
	pub := &capturePublisher{}
	return Deps{Store: st, Events: pub}, pub
}

func TestEmitDeployEvent(t *testing.T) {
	tests := []struct {
		name         string
		status       string
		wantSeverity string
	}{
		{name: "deployed is info", status: "deployed", wantSeverity: "info"},
		{name: "deploying is info", status: "deploying", wantSeverity: "info"},
		{name: "failed is error", status: "failed: pull foo failed", wantSeverity: "error"},
		{name: "failed bare is error", status: "failed", wantSeverity: "error"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			deps, pub := newEmitDeps(t)
			w := Workload{ID: "wl-123", Name: "my-app"}

			EmitDeployEvent(deps, w, "image", tt.status)

			// Persisted row carries the workload scope + derived severity.
			rows, err := deps.Store.ListEvents(store.EventLogFilter{WorkloadID: w.ID})
			if err != nil {
				t.Fatalf("ListEvents: %v", err)
			}
			if len(rows) != 1 {
				t.Fatalf("got %d persisted events, want 1", len(rows))
			}
			got := rows[0]
			if got.Severity != tt.wantSeverity {
				t.Errorf("severity = %q, want %q", got.Severity, tt.wantSeverity)
			}
			if got.Source != "image" {
				t.Errorf("source = %q, want %q", got.Source, "image")
			}
			if got.WorkloadID != w.ID {
				t.Errorf("workload_id = %q, want %q", got.WorkloadID, w.ID)
			}
			wantMsg := w.Name + ": " + tt.status
			if got.Message != wantMsg {
				t.Errorf("message = %q, want %q", got.Message, wantMsg)
			}

			// Metadata JSON carries workload_id / workload_name / status.
			var meta map[string]string
			if err := json.Unmarshal([]byte(got.Metadata), &meta); err != nil {
				t.Fatalf("unmarshal metadata %q: %v", got.Metadata, err)
			}
			if meta["workload_id"] != w.ID {
				t.Errorf("metadata workload_id = %q, want %q", meta["workload_id"], w.ID)
			}
			if meta["workload_name"] != w.Name {
				t.Errorf("metadata workload_name = %q, want %q", meta["workload_name"], w.Name)
			}
			if meta["status"] != tt.status {
				t.Errorf("metadata status = %q, want %q", meta["status"], tt.status)
			}

			// The persisted row is also re-published on the bus as an
			// EventLog so SSE clients see it live.
			if len(pub.events) != 1 {
				t.Fatalf("got %d published events, want 1", len(pub.events))
			}
			ev := pub.events[0]
			if ev.Type != events.EventLog {
				t.Errorf("event type = %q, want %q", ev.Type, events.EventLog)
			}
			payload, ok := ev.Payload.(events.EventLogPayload)
			if !ok {
				t.Fatalf("payload type = %T, want events.EventLogPayload", ev.Payload)
			}
			if payload.WorkloadID != w.ID {
				t.Errorf("payload workload_id = %q, want %q", payload.WorkloadID, w.ID)
			}
			if payload.Severity != tt.wantSeverity {
				t.Errorf("payload severity = %q, want %q", payload.Severity, tt.wantSeverity)
			}
			if payload.ID != got.ID {
				t.Errorf("payload id = %d, want %d", payload.ID, got.ID)
			}
		})
	}
}

// TestEmitDeployEvent_CapsLongStatus verifies a long failure status (e.g. one
// embedding raw subprocess output) is bounded to maxDeployStatusRunes runes in
// both the persisted message and metadata, cut on a UTF-8 boundary, while
// severity is still derived from the original "failed" prefix.
func TestEmitDeployEvent_CapsLongStatus(t *testing.T) {
	deps, pub := newEmitDeps(t)
	w := Workload{ID: "wl-cap", Name: "app"}

	// Multibyte body so a naive byte-slice would corrupt a rune; prefix with
	// "failed: " so the severity check exercises the pre-cap derivation.
	longStatus := "failed: " + strings.Repeat("é", 400)
	EmitDeployEvent(deps, w, "compose", longStatus)

	rows, err := deps.Store.ListEvents(store.EventLogFilter{WorkloadID: w.ID})
	if err != nil {
		t.Fatalf("ListEvents: %v", err)
	}
	if len(rows) != 1 {
		t.Fatalf("got %d events, want 1", len(rows))
	}
	got := rows[0]

	if got.Severity != "error" {
		t.Errorf("severity = %q, want error (derived from pre-cap prefix)", got.Severity)
	}

	var meta map[string]string
	if err := json.Unmarshal([]byte(got.Metadata), &meta); err != nil {
		t.Fatalf("unmarshal metadata: %v", err)
	}
	capped := meta["status"]
	if rc := len([]rune(capped)); rc != maxDeployStatusRunes+1 { // +1 for the ellipsis rune
		t.Errorf("capped status = %d runes, want %d", rc, maxDeployStatusRunes+1)
	}
	if !utf8.ValidString(capped) {
		t.Errorf("capped status is not valid UTF-8: %q", capped)
	}
	if !strings.HasSuffix(capped, "…") {
		t.Errorf("capped status missing ellipsis suffix: %q", capped)
	}
	wantMsg := w.Name + ": " + capped
	if got.Message != wantMsg {
		t.Errorf("message = %q, want %q", got.Message, wantMsg)
	}
	if len(pub.events) != 1 {
		t.Fatalf("got %d published events, want 1", len(pub.events))
	}
}
