package cron_test

import (
	"context"
	"errors"
	"log/slog"
	"os"
	"path/filepath"
	"sync/atomic"
	"testing"
	"time"

	"github.com/enmanuel/agents/internal/config"
	coretypes "github.com/enmanuel/agents/pkg/llm"
	shellcron "github.com/enmanuel/agents/shell/cron"
)

// ── fakes ──────────────────────────────────────────────────────────────────

type fakeSender struct {
	calls  atomic.Int32
	lastMD string
	lastRM string
}

func (f *fakeSender) SendMarkdown(_ context.Context, room, md string) error {
	f.calls.Add(1)
	f.lastRM = room
	f.lastMD = md
	return nil
}

type errSender struct{}

func (e *errSender) SendMarkdown(_ context.Context, _, _ string) error {
	return errors.New("matrix unavailable")
}

func fakeLLM(reply string) coretypes.CompleteFunc {
	return func(_ context.Context, _ coretypes.CompletionRequest) (coretypes.CompletionResponse, error) {
		return coretypes.CompletionResponse{Content: reply}, nil
	}
}

func newTestLogger(t *testing.T) *slog.Logger {
	t.Helper()
	return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))
}

// ── helpers ────────────────────────────────────────────────────────────────

// waitCalls blocks until the sender has received at least n calls or the deadline passes.
func waitCalls(t *testing.T, f *fakeSender, n int32) {
	t.Helper()
	deadline := time.Now().Add(3 * time.Second)
	for time.Now().Before(deadline) {
		if f.calls.Load() >= n {
			return
		}
		time.Sleep(20 * time.Millisecond)
	}
	t.Fatalf("expected %d call(s) to SendMarkdown, got %d", n, f.calls.Load())
}

// ── cron-based tests (require timer) ──────────────────────────────────────

func TestScheduler_SendMessage_Inline(t *testing.T) {
	sender := &fakeSender{}
	cfg := []config.ScheduleCfg{
		{
			Name:       "test-inline",
			Cron:       "@every 100ms",
			OutputRoom: "!room:server.com",
			Action: config.ScheduledAction{
				Kind:    "send_message",
				Message: "hola mundo",
			},
		},
	}

	s := shellcron.New(cfg, sender, nil, "", newTestLogger(t))

	ctx, cancel := context.WithCancel(context.Background())
	done := make(chan struct{})
	go func() {
		defer close(done)
		s.Start(ctx)
	}()

	waitCalls(t, sender, 1)
	cancel()
	<-done

	if sender.lastRM != "!room:server.com" {
		t.Errorf("unexpected room: %s", sender.lastRM)
	}
	if sender.lastMD != "hola mundo" {
		t.Errorf("unexpected message: %s", sender.lastMD)
	}
}

func TestScheduler_SendMessage_Template(t *testing.T) {
	// Write a temporary template file
	dir := t.TempDir()
	tmpl := filepath.Join(dir, "greeting.md")
	if err := os.WriteFile(tmpl, []byte("buenos días"), 0o600); err != nil {
		t.Fatal(err)
	}

	sender := &fakeSender{}
	cfg := []config.ScheduleCfg{
		{
			Name:       "test-template",
			Cron:       "@every 100ms",
			OutputRoom: "!room2:server.com",
			Action: config.ScheduledAction{
				Kind:     "send_message",
				Template: tmpl,
			},
		},
	}

	s := shellcron.New(cfg, sender, nil, "", newTestLogger(t))

	ctx, cancel := context.WithCancel(context.Background())
	done := make(chan struct{})
	go func() {
		defer close(done)
		s.Start(ctx)
	}()

	waitCalls(t, sender, 1)
	cancel()
	<-done

	if sender.lastMD != "buenos días" {
		t.Errorf("unexpected message: %q", sender.lastMD)
	}
}

func TestScheduler_LLMPrompt(t *testing.T) {
	sender := &fakeSender{}
	cfg := []config.ScheduleCfg{
		{
			Name:       "test-llm",
			Cron:       "@every 100ms",
			OutputRoom: "!room3:server.com",
			Action: config.ScheduledAction{
				Kind:   "llm_prompt",
				Prompt: "resume el día",
			},
		},
	}

	llm := fakeLLM("resumen generado por LLM")
	s := shellcron.New(cfg, sender, llm, "gpt-4o", newTestLogger(t))

	ctx, cancel := context.WithCancel(context.Background())
	done := make(chan struct{})
	go func() {
		defer close(done)
		s.Start(ctx)
	}()

	waitCalls(t, sender, 1)
	cancel()
	<-done

	if sender.lastMD != "resumen generado por LLM" {
		t.Errorf("unexpected LLM reply: %q", sender.lastMD)
	}
}

func TestScheduler_MatrixSendError(t *testing.T) {
	// If matrix.SendMarkdown returns an error, the scheduler should log it and not panic.
	cfg := []config.ScheduleCfg{
		{
			Name:       "err-send",
			Cron:       "@every 100ms",
			OutputRoom: "!room:server.com",
			Action: config.ScheduledAction{
				Kind:    "send_message",
				Message: "trigger error",
			},
		},
	}

	s := shellcron.New(cfg, &errSender{}, nil, "", newTestLogger(t))

	ctx, cancel := context.WithCancel(context.Background())
	done := make(chan struct{})
	go func() {
		defer close(done)
		s.Start(ctx)
	}()

	// Let it fire at least once without panicking
	time.Sleep(250 * time.Millisecond)
	cancel()
	<-done
}

// ── Fire() tests (deterministic, no timer) ─────────────────────────────────

func TestFire_SendMessage_Inline(t *testing.T) {
	sender := &fakeSender{}
	s := shellcron.New(nil, sender, nil, "", newTestLogger(t))

	sc := config.ScheduleCfg{
		Name:       "fire-inline",
		Cron:       "0 9 * * *",
		OutputRoom: "!fireroom:server.com",
		Action: config.ScheduledAction{
			Kind:    "send_message",
			Message: "buenos días via Fire",
		},
	}

	s.Fire(context.Background(), sc)

	if sender.calls.Load() != 1 {
		t.Fatalf("expected 1 call, got %d", sender.calls.Load())
	}
	if sender.lastRM != "!fireroom:server.com" {
		t.Errorf("unexpected room: %s", sender.lastRM)
	}
	if sender.lastMD != "buenos días via Fire" {
		t.Errorf("unexpected message: %s", sender.lastMD)
	}
}

func TestFire_LLMPrompt(t *testing.T) {
	sender := &fakeSender{}
	llm := fakeLLM("respuesta del LLM via Fire")
	s := shellcron.New(nil, sender, llm, "gpt-4o", newTestLogger(t))

	sc := config.ScheduleCfg{
		Name:       "fire-llm",
		Cron:       "0 18 * * *",
		OutputRoom: "!llmroom:server.com",
		Action: config.ScheduledAction{
			Kind:   "llm_prompt",
			Prompt: "resume el día",
		},
	}

	s.Fire(context.Background(), sc)

	if sender.calls.Load() != 1 {
		t.Fatalf("expected 1 call, got %d", sender.calls.Load())
	}
	if sender.lastMD != "respuesta del LLM via Fire" {
		t.Errorf("unexpected LLM reply: %q", sender.lastMD)
	}
}

func TestFire_NoOutputRoom_Skips(t *testing.T) {
	sender := &fakeSender{}
	s := shellcron.New(nil, sender, nil, "", newTestLogger(t))

	sc := config.ScheduleCfg{
		Name:       "fire-no-room",
		Cron:       "0 9 * * *",
		OutputRoom: "", // intentionally empty
		Action: config.ScheduledAction{
			Kind:    "send_message",
			Message: "should not send",
		},
	}

	s.Fire(context.Background(), sc)

	if sender.calls.Load() != 0 {
		t.Errorf("expected 0 calls when output_room is empty, got %d", sender.calls.Load())
	}
}

func TestFire_LLMPrompt_NoLLM_Skips(t *testing.T) {
	// When no LLM is configured, Fire with llm_prompt should not send anything.
	sender := &fakeSender{}
	s := shellcron.New(nil, sender, nil, "", newTestLogger(t))

	sc := config.ScheduleCfg{
		Name:       "fire-no-llm",
		Cron:       "0 9 * * *",
		OutputRoom: "!room:server.com",
		Action: config.ScheduledAction{
			Kind:   "llm_prompt",
			Prompt: "hello",
		},
	}

	s.Fire(context.Background(), sc)

	if sender.calls.Load() != 0 {
		t.Errorf("expected 0 calls without LLM, got %d", sender.calls.Load())
	}
}

func TestScheduler_SkipsInvalidSchedule(t *testing.T) {
	// Schedules without output_room or without action kind must be skipped during Start.
	// We use Fire directly to test the skip logic without timer overhead.
	sender := &fakeSender{}
	s := shellcron.New(nil, sender, nil, "", newTestLogger(t))
	ctx := context.Background()

	// No output_room → skip
	s.Fire(ctx, config.ScheduleCfg{
		Name: "no-room",
		Cron: "@every 100ms",
		// missing OutputRoom
		Action: config.ScheduledAction{Kind: "send_message", Message: "hi"},
	})

	// No kind → Fire calls buildHandler which returns nil → skip
	s.Fire(ctx, config.ScheduleCfg{
		Name:       "no-kind",
		Cron:       "@every 100ms",
		OutputRoom: "!room:server.com",
		// missing Action.Kind
	})

	if sender.calls.Load() != 0 {
		t.Errorf("expected 0 calls for invalid schedules, got %d", sender.calls.Load())
	}
}
