package schedule

import (
	"context"
	"encoding/json"
	"strings"
	"testing"
	"time"

	"github.com/alexei/tinyforge/internal/workload/plugin"
)

func TestValidate(t *testing.T) {
	tr := &trigger{}
	cases := []struct {
		name    string
		cfg     json.RawMessage
		wantErr string // substring; empty = expect success
	}{
		{"empty body rejected", nil, "config is required"},
		{"empty object rejected", json.RawMessage(`{}`), "interval is required"},
		{"missing interval rejected", json.RawMessage(`{"reference":"v1"}`), "interval is required"},
		{"invalid json rejected", json.RawMessage(`not json`), "invalid json"},
		{"unparseable interval rejected", json.RawMessage(`{"interval":"banana"}`), "invalid interval"},
		{"zero interval rejected", json.RawMessage(`{"interval":"0s"}`), "must be positive"},
		{"sub-minute rejected", json.RawMessage(`{"interval":"30s"}`), "below minimum"},
		{"exact minimum accepted", json.RawMessage(`{"interval":"1m"}`), ""},
		{"hour accepted", json.RawMessage(`{"interval":"1h"}`), ""},
		{"day accepted", json.RawMessage(`{"interval":"24h"}`), ""},
		{"unknown keys tolerated", json.RawMessage(`{"interval":"1h","future_field":42}`), ""},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := tr.Validate(tc.cfg)
			if tc.wantErr == "" {
				if err != nil {
					t.Fatalf("expected nil err, got %v", err)
				}
				return
			}
			if err == nil {
				t.Fatalf("expected error containing %q, got nil", tc.wantErr)
			}
			if !strings.Contains(err.Error(), tc.wantErr) {
				t.Fatalf("error %q missing substring %q", err.Error(), tc.wantErr)
			}
		})
	}
}

func TestParseInterval(t *testing.T) {
	cases := []struct {
		in   string
		want time.Duration
		err  bool
	}{
		{"1h", time.Hour, false},
		{"24h", 24 * time.Hour, false},
		{"  5m  ", 5 * time.Minute, false},
		{"168h", 7 * 24 * time.Hour, false},
		{"", 0, true},
		{"banana", 0, true},
		{"-1h", 0, true},
		{"0s", 0, true},
	}
	for _, tc := range cases {
		t.Run(tc.in, func(t *testing.T) {
			got, err := ParseInterval(tc.in)
			if tc.err {
				if err == nil {
					t.Fatalf("expected error, got nil")
				}
				return
			}
			if err != nil {
				t.Fatalf("unexpected err: %v", err)
			}
			if got != tc.want {
				t.Fatalf("got %s, want %s", got, tc.want)
			}
		})
	}
}

func TestIntervalOfRaw(t *testing.T) {
	d, err := IntervalOfRaw(`{"interval":"30m"}`)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	if d != 30*time.Minute {
		t.Fatalf("got %s, want 30m", d)
	}
	if _, err := IntervalOfRaw(""); err == nil {
		t.Fatalf("expected error on empty raw")
	}
}

func TestMatch_WrongKindIgnored(t *testing.T) {
	tr := &trigger{}
	wl := plugin.Workload{ID: "w1", TriggerConfig: json.RawMessage(`{"interval":"1h"}`)}
	intent, err := tr.Match(context.Background(), plugin.Deps{}, wl,
		plugin.InboundEvent{Kind: "manual"})
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if intent != nil {
		t.Fatalf("expected nil intent for wrong kind, got %+v", intent)
	}
}

func TestMatch_MissingSchedulePayload(t *testing.T) {
	tr := &trigger{}
	wl := plugin.Workload{ID: "w1", TriggerConfig: json.RawMessage(`{"interval":"1h"}`)}
	intent, err := tr.Match(context.Background(), plugin.Deps{}, wl,
		plugin.InboundEvent{Kind: "schedule"})
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if intent != nil {
		t.Fatalf("expected nil intent when payload missing, got %+v", intent)
	}
}

func TestMatch_SchedulePopulatesIntent(t *testing.T) {
	tr := &trigger{}
	wl := plugin.Workload{
		ID:            "w1",
		TriggerConfig: json.RawMessage(`{"interval":"6h","reference":"stable"}`),
	}
	fixed := time.Date(2026, 5, 16, 12, 0, 0, 0, time.UTC)
	intent, err := tr.Match(context.Background(), plugin.Deps{}, wl,
		plugin.InboundEvent{
			Kind:     "schedule",
			Schedule: &plugin.ScheduleEvent{FiredAt: fixed},
		})
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if intent == nil {
		t.Fatalf("expected intent")
	}
	if intent.Reason != "schedule" {
		t.Errorf("Reason = %q, want schedule", intent.Reason)
	}
	if intent.Reference != "stable" {
		t.Errorf("Reference = %q, want stable", intent.Reference)
	}
	if intent.TriggeredBy != "scheduler" {
		t.Errorf("TriggeredBy = %q, want scheduler", intent.TriggeredBy)
	}
	if !intent.TriggeredAt.Equal(fixed) {
		t.Errorf("TriggeredAt = %s, want %s", intent.TriggeredAt, fixed)
	}
	if intent.Metadata["interval"] != "6h" {
		t.Errorf("interval metadata = %q, want 6h", intent.Metadata["interval"])
	}
}

func TestMatch_ZeroFiredAtFallsBackToNow(t *testing.T) {
	tr := &trigger{}
	wl := plugin.Workload{
		ID:            "w1",
		TriggerConfig: json.RawMessage(`{"interval":"1h"}`),
	}
	before := time.Now().UTC()
	intent, err := tr.Match(context.Background(), plugin.Deps{}, wl,
		plugin.InboundEvent{
			Kind:     "schedule",
			Schedule: &plugin.ScheduleEvent{},
		})
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	after := time.Now().UTC()
	if intent.TriggeredAt.Before(before) || intent.TriggeredAt.After(after.Add(time.Second)) {
		t.Errorf("TriggeredAt %s not in [%s, %s]", intent.TriggeredAt, before, after)
	}
}

func TestKindAndSchemaSample(t *testing.T) {
	tr := &trigger{}
	if tr.Kind() != "schedule" {
		t.Fatalf("Kind = %q, want schedule", tr.Kind())
	}
	type withSample interface{ SchemaSample() any }
	s, ok := any(tr).(withSample)
	if !ok {
		t.Fatalf("trigger does not expose SchemaSample()")
	}
	sample, ok := s.SchemaSample().(Config)
	if !ok {
		t.Fatalf("SchemaSample is not Config: %T", s.SchemaSample())
	}
	if _, err := ParseInterval(sample.Interval); err != nil {
		t.Errorf("SchemaSample.Interval is not parseable: %v", err)
	}
}
