package events

import (
	"errors"
	"regexp"
	"sync"
	"testing"
	"time"

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

// fakeTriggerSource lets tests inject a static set of enabled triggers
// without standing up SQLite. ListEnabledEventTriggers is the only
// method the dispatcher uses.
type fakeTriggerSource struct {
	rows []store.EventTrigger
	err  error
}

func (f *fakeTriggerSource) ListEnabledEventTriggers() ([]store.EventTrigger, error) {
	if f.err != nil {
		return nil, f.err
	}
	return f.rows, nil
}

// fakeNotifier captures dispatches in memory so tests can assert
// (URL, secret, eventType, payload) tuples.
type fakeNotifier struct {
	mu    sync.Mutex
	calls []fakeNotifierCall
}

type fakeNotifierCall struct {
	URL       string
	Secret    string
	EventType string
	Payload   any
}

func (f *fakeNotifier) SendPayload(url, secret, eventType string, payload any) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.calls = append(f.calls, fakeNotifierCall{URL: url, Secret: secret, EventType: eventType, Payload: payload})
}

func (f *fakeNotifier) Calls() []fakeNotifierCall {
	f.mu.Lock()
	defer f.mu.Unlock()
	out := make([]fakeNotifierCall, len(f.calls))
	copy(out, f.calls)
	return out
}

func newDispatcher(rows []store.EventTrigger) (*dispatcher, *fakeNotifier) {
	n := &fakeNotifier{}
	return &dispatcher{
		triggers:   &fakeTriggerSource{rows: rows},
		notifier:   n,
		regexCache: map[string]*regexp.Regexp{},
	}, n
}

func TestMatches_EmptyFiltersAllowAnything(t *testing.T) {
	d, _ := newDispatcher(nil)
	tr := store.EventTrigger{Name: "anything"}
	got, err := d.matches(tr, EventLogPayload{Severity: "info", Source: "deploy", Message: "hello"})
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if !got {
		t.Fatal("empty filters should pass")
	}
}

func TestMatches_SeverityCSV(t *testing.T) {
	d, _ := newDispatcher(nil)
	tr := store.EventTrigger{FilterSeverity: "warn, error"}
	cases := []struct {
		sev  string
		want bool
	}{
		{"error", true},
		{"warn", true},
		{"info", false},
	}
	for _, c := range cases {
		got, err := d.matches(tr, EventLogPayload{Severity: c.sev})
		if err != nil {
			t.Fatalf("err: %v", err)
		}
		if got != c.want {
			t.Errorf("severity=%q want=%v got=%v", c.sev, c.want, got)
		}
	}
}

func TestMatches_SourceCSV(t *testing.T) {
	d, _ := newDispatcher(nil)
	tr := store.EventTrigger{FilterSource: "logscan,deploy"}
	cases := []struct {
		src  string
		want bool
	}{
		{"logscan", true},
		{"deploy", true},
		{"reconciler", false},
		{"", false},
	}
	for _, c := range cases {
		got, _ := d.matches(tr, EventLogPayload{Source: c.src})
		if got != c.want {
			t.Errorf("source=%q want=%v got=%v", c.src, c.want, got)
		}
	}
}

func TestMatches_MessageRegex(t *testing.T) {
	d, _ := newDispatcher(nil)
	tr := store.EventTrigger{FilterMessageRegex: `(?i)\bpanic\b`}
	if got, _ := d.matches(tr, EventLogPayload{Message: "fatal: Panic in worker"}); !got {
		t.Error("expected case-insensitive panic to match")
	}
	if got, _ := d.matches(tr, EventLogPayload{Message: "all good"}); got {
		t.Error("expected non-matching message to fail")
	}
}

func TestMatches_InvalidRegexReturnsError(t *testing.T) {
	d, _ := newDispatcher(nil)
	tr := store.EventTrigger{FilterMessageRegex: "([unclosed"}
	_, err := d.matches(tr, EventLogPayload{Message: "x"})
	if err == nil {
		t.Fatal("expected compile error on invalid regex")
	}
}

func TestMatches_AllFiltersAND(t *testing.T) {
	d, _ := newDispatcher(nil)
	tr := store.EventTrigger{
		FilterSeverity:     "error",
		FilterSource:       "logscan",
		FilterMessageRegex: "timeout",
	}
	full := EventLogPayload{Severity: "error", Source: "logscan", Message: "request timeout"}
	if got, _ := d.matches(tr, full); !got {
		t.Error("all-match payload should pass")
	}
	missMessage := full
	missMessage.Message = "all good"
	if got, _ := d.matches(tr, missMessage); got {
		t.Error("message mismatch should fail despite severity+source match")
	}
	missSource := full
	missSource.Source = "deploy"
	if got, _ := d.matches(tr, missSource); got {
		t.Error("source mismatch should fail")
	}
}

func TestHandle_DispatchesMatchingTrigger(t *testing.T) {
	rows := []store.EventTrigger{
		{ID: 1, Name: "T1", FilterSeverity: "error", ActionType: store.EventTriggerActionWebhook,
			ActionTarget: "https://example.com/hook", ActionSecret: "shh"},
		{ID: 2, Name: "T2", FilterSeverity: "warn", ActionType: store.EventTriggerActionWebhook,
			ActionTarget: "https://example.com/other"},
	}
	d, notifier := newDispatcher(rows)
	d.handle(EventLogPayload{Severity: "error", Source: "logscan", Message: "panic", CreatedAt: time.Now().Format(time.RFC3339)})

	calls := notifier.Calls()
	if len(calls) != 1 {
		t.Fatalf("expected 1 call, got %d", len(calls))
	}
	if calls[0].URL != "https://example.com/hook" {
		t.Errorf("URL=%q want https://example.com/hook", calls[0].URL)
	}
	if calls[0].Secret != "shh" {
		t.Errorf("Secret=%q want shh", calls[0].Secret)
	}
	p, ok := calls[0].Payload.(TriggerWebhookPayload)
	if !ok {
		t.Fatalf("payload type=%T want TriggerWebhookPayload", calls[0].Payload)
	}
	if p.TriggerID != 1 || p.Trigger != "T1" {
		t.Errorf("payload trigger metadata wrong: %+v", p)
	}
	if p.Event.Severity != "error" {
		t.Errorf("payload event mismatch: %+v", p.Event)
	}
}

func TestHandle_TriggerSourceErrorLogged(t *testing.T) {
	n := &fakeNotifier{}
	d := &dispatcher{
		triggers:   &fakeTriggerSource{err: errors.New("db down")},
		notifier:   n,
		regexCache: map[string]*regexp.Regexp{},
	}
	d.handle(EventLogPayload{Severity: "error"})
	if len(n.Calls()) != 0 {
		t.Errorf("dispatcher should not call notifier when trigger source errored")
	}
}

func TestHandle_UnsupportedActionTypeSkipped(t *testing.T) {
	rows := []store.EventTrigger{
		{ID: 1, Name: "T1", ActionType: "future-channel", ActionTarget: "x"},
	}
	d, n := newDispatcher(rows)
	d.handle(EventLogPayload{Severity: "info"})
	if len(n.Calls()) != 0 {
		t.Errorf("unsupported action_type should not dispatch")
	}
}

func TestFilterMatchCSV(t *testing.T) {
	cases := []struct {
		filter, cand string
		want         bool
	}{
		{"", "anything", true},
		{"  ", "anything", true},
		{"a", "a", true},
		{"a", "b", false},
		{"a,b,c", "b", true},
		{" a , b , c ", "b", true},
		{"a,b", "c", false},
	}
	for _, c := range cases {
		got := filterMatchCSV(c.filter, c.cand)
		if got != c.want {
			t.Errorf("filterMatchCSV(%q, %q) = %v want %v", c.filter, c.cand, got, c.want)
		}
	}
}

func TestRegexCache_ReusesCompiledPattern(t *testing.T) {
	d, _ := newDispatcher(nil)
	re1, err := d.compile(`\bfoo\b`)
	if err != nil {
		t.Fatalf("compile: %v", err)
	}
	re2, err := d.compile(`\bfoo\b`)
	if err != nil {
		t.Fatalf("recompile: %v", err)
	}
	if re1 != re2 {
		t.Error("expected cached compile to return the same regexp pointer")
	}
}
