package app

import (
	"context"
	"path/filepath"
	"testing"
	"time"

	"gitea.mixdep.ru/mix/gosentry/src/domain"
	"gitea.mixdep.ru/mix/gosentry/src/storage"
)

// newTempService builds a Service backed by a store rooted in a temp directory,
// so the mutating operations can persist to real (throwaway) files.
func newTempService(t *testing.T, jobs []domain.Job) *Service {
	t.Helper()
	dir := t.TempDir()
	store := &storage.Store{
		Paths: storage.Paths{
			ExecutablePath: filepath.Join(dir, "gosentry"),
			AppDir:         dir,
			ConfigPath:     filepath.Join(dir, "gosentry.yaml"),
			JobsDir:        dir,
			JobsPath:       filepath.Join(dir, "jobs.yaml"),
			LogsDir:        filepath.Join(dir, "logs"),
		},
		Config: domain.Config{JobsDir: ".", LogsDir: "logs", MaxLogFiles: 100, MaxLogAgeDays: 30},
	}
	return NewService(store, jobs)
}

// recorder is a test observer that captures every emitted event.
type recorder struct {
	events []Event
}

func (r *recorder) OnEvent(e Event) { r.events = append(r.events, e) }

func (r *recorder) jobChanged() (ids []int) {
	for _, e := range r.events {
		if jc, ok := e.(JobChanged); ok {
			ids = append(ids, jc.JobID)
		}
	}
	return ids
}

func (r *recorder) records() (out []domain.RunRecord) {
	for _, e := range r.events {
		if rr, ok := e.(RunRecorded); ok {
			out = append(out, rr.Record)
		}
	}
	return out
}

func TestCreateJobAssignsIDAndEmits(t *testing.T) {
	svc := newTempService(t, nil)
	rec := &recorder{}
	svc.Subscribe(rec)

	created, err := svc.CreateJob(domain.Job{Name: "Build", Schedule: "@every 1m", Command: "echo hi", Enabled: true})
	if err != nil {
		t.Fatalf("CreateJob: %v", err)
	}
	if created.ID != 1 {
		t.Errorf("first job ID = %d, want 1", created.ID)
	}
	if got := svc.Jobs(); len(got) != 1 || got[0].Name != "Build" {
		t.Fatalf("jobs after create = %+v", got)
	}
	if rt := svc.Runtime(1); rt == nil || rt.LastState != "Ready" {
		t.Errorf("runtime = %+v, want LastState Ready", rt)
	}
	if recs := rec.records(); len(recs) != 1 || recs[0].State != "Created" {
		t.Errorf("records = %+v, want one Created", recs)
	}
	if ids := rec.jobChanged(); len(ids) != 1 || ids[0] != 1 {
		t.Errorf("JobChanged ids = %v, want [1]", ids)
	}

	// A second job takes the next free ID.
	second, err := svc.CreateJob(domain.Job{Name: "Two", Schedule: "@every 1m", Command: "echo two"})
	if err != nil {
		t.Fatalf("CreateJob 2: %v", err)
	}
	if second.ID != 2 {
		t.Errorf("second job ID = %d, want 2", second.ID)
	}
}

func TestCreateJobValidates(t *testing.T) {
	svc := newTempService(t, nil)
	if _, err := svc.CreateJob(domain.Job{Schedule: "@every 1m", Command: "echo"}); err == nil {
		t.Error("expected error for missing name")
	}
	if got := svc.Jobs(); len(got) != 0 {
		t.Errorf("invalid job should not be stored, jobs = %+v", got)
	}
}

func TestUpdateJobKeepsRuntimeAndReflectsDisable(t *testing.T) {
	svc := newTempService(t, []domain.Job{{ID: 5, Name: "Old", Schedule: "@every 1m", Command: "echo", Enabled: true}})
	if err := svc.UpdateJob(domain.Job{ID: 5, Name: "New", Schedule: "@every 1m", Command: "echo", Enabled: false}); err != nil {
		t.Fatalf("UpdateJob: %v", err)
	}
	got := svc.Jobs()
	if got[0].Name != "New" || got[0].Enabled {
		t.Errorf("job after update = %+v", got[0])
	}
	if rt := svc.Runtime(5); rt == nil || rt.LastState != "Paused" || rt.NextRun != "Paused" {
		t.Errorf("runtime after disable = %+v", rt)
	}
}

func TestUpdateJobNotFound(t *testing.T) {
	svc := newTempService(t, nil)
	if err := svc.UpdateJob(domain.Job{ID: 99, Name: "X", Schedule: "@every 1m", Command: "echo"}); err == nil {
		t.Error("expected not-found error")
	}
}

func TestDeleteJobRemovesEverything(t *testing.T) {
	svc := newTempService(t, []domain.Job{{ID: 1, Name: "A", Schedule: "@every 1m", Command: "echo", Enabled: true}})
	rec := &recorder{}
	svc.Subscribe(rec)

	if err := svc.DeleteJob(1); err != nil {
		t.Fatalf("DeleteJob: %v", err)
	}
	if got := svc.Jobs(); len(got) != 0 {
		t.Errorf("jobs after delete = %+v", got)
	}
	if rt := svc.Runtime(1); rt != nil {
		t.Errorf("runtime should be gone, got %+v", rt)
	}
	if recs := rec.records(); len(recs) != 1 || recs[0].State != "Deleted" {
		t.Errorf("records = %+v, want one Deleted", recs)
	}
	if ids := rec.jobChanged(); len(ids) != 1 || ids[0] != 0 {
		t.Errorf("JobChanged ids = %v, want [0] (broad)", ids)
	}
}

func TestSetEnabledToggles(t *testing.T) {
	svc := newTempService(t, []domain.Job{{ID: 1, Name: "A", Schedule: "@every 1m", Command: "echo", Enabled: false}})

	if err := svc.SetEnabled(1, true); err != nil {
		t.Fatalf("SetEnabled true: %v", err)
	}
	if rt := svc.Runtime(1); rt.LastState != "Ready" || rt.NextDue.IsZero() {
		t.Errorf("enabled runtime = %+v, want Ready with a next-due", rt)
	}
	if err := svc.SetEnabled(1, false); err != nil {
		t.Fatalf("SetEnabled false: %v", err)
	}
	if rt := svc.Runtime(1); rt.LastState != "Paused" || !rt.NextDue.IsZero() {
		t.Errorf("disabled runtime = %+v, want Paused with no next-due", rt)
	}
}

func TestSetGlobalPauseUpdatesRuntimesAndEmits(t *testing.T) {
	svc := newTempService(t, []domain.Job{
		{ID: 1, Name: "On", Schedule: "@every 1m", Command: "echo", Enabled: true},
		{ID: 2, Name: "Off", Schedule: "@every 1m", Command: "echo", Enabled: false},
	})
	rec := &recorder{}
	svc.Subscribe(rec)

	if err := svc.SetGlobalPause(true); err != nil {
		t.Fatalf("SetGlobalPause: %v", err)
	}
	if rt := svc.Runtime(1); rt.NextRun != "Scheduler paused" {
		t.Errorf("enabled job next-run = %q, want %q", rt.NextRun, "Scheduler paused")
	}
	if rt := svc.Runtime(2); rt.NextRun != "Paused" {
		t.Errorf("disabled job next-run = %q, want %q", rt.NextRun, "Paused")
	}
	var sawState bool
	for _, e := range rec.events {
		if ss, ok := e.(SchedulerStateChanged); ok && ss.Paused {
			sawState = true
		}
	}
	if !sawState {
		t.Error("expected a SchedulerStateChanged{Paused:true} event")
	}

	// Resuming recomputes a real next run for the enabled job.
	if err := svc.SetGlobalPause(false); err != nil {
		t.Fatalf("resume: %v", err)
	}
	if rt := svc.Runtime(1); rt.NextDue.IsZero() {
		t.Errorf("resumed enabled job should have a next-due, got %+v", rt)
	}
}

func TestRunNowUsesRunnerAndRecords(t *testing.T) {
	svc := newTempService(t, []domain.Job{{ID: 1, Name: "A", Schedule: "@every 1m", Command: "echo", Enabled: true}})

	done := make(chan domain.RunRecord, 1)
	svc.runJob = func(_ context.Context, job *domain.Job, trigger string, _ string) domain.RunRecord {
		if trigger != "Manual" {
			t.Errorf("trigger = %q, want Manual", trigger)
		}
		return domain.RunRecord{Time: "2026-06-19 12:00:00", JobID: job.ID, JobName: job.Name, State: "Success", Output: "ok"}
	}
	svc.Subscribe(ObserverFunc(func(e Event) {
		if rr, ok := e.(RunRecorded); ok && rr.Record.JobID == 1 {
			select {
			case done <- rr.Record:
			default:
			}
		}
	}))

	if err := svc.RunNow(1); err != nil {
		t.Fatalf("RunNow: %v", err)
	}

	select {
	case record := <-done:
		if record.State != "Success" {
			t.Errorf("recorded state = %q, want Success", record.State)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timed out waiting for run to be recorded")
	}

	if rt := svc.Runtime(1); rt.LastState != "Success" || rt.Output != "ok" {
		t.Errorf("runtime after run = %+v", rt)
	}
}

func TestRunNowRefusedWhilePaused(t *testing.T) {
	svc := newTempService(t, []domain.Job{{ID: 1, Name: "A", Schedule: "@every 1m", Command: "echo", Enabled: true}})
	var ran bool
	svc.runJob = func(context.Context, *domain.Job, string, string) domain.RunRecord {
		ran = true
		return domain.RunRecord{}
	}
	if err := svc.SetGlobalPause(true); err != nil {
		t.Fatalf("SetGlobalPause: %v", err)
	}
	if err := svc.RunNow(1); err == nil {
		t.Error("expected RunNow to be refused while paused")
	}
	if ran {
		t.Error("runner must not be invoked while paused")
	}
}

func TestUpdateSettingsPersistsAndValidates(t *testing.T) {
	svc := newTempService(t, nil)

	bad := svc.store.Config
	bad.MaxLogFiles = 0
	if err := svc.UpdateSettings(bad); err == nil {
		t.Error("expected validation error for non-positive max log files")
	}

	good := svc.store.Config
	good.NotifyOnFailure = false
	good.MaxLogAgeDays = 7
	if err := svc.UpdateSettings(good); err != nil {
		t.Fatalf("UpdateSettings: %v", err)
	}
	if svc.Store().Config.MaxLogAgeDays != 7 || svc.Store().Config.NotifyOnFailure {
		t.Errorf("config not applied: %+v", svc.Store().Config)
	}
}
