package task

import (
	"context"
	"encoding/json"
	"errors"
	"sync"
	"testing"
	"time"
)

type applicationTaskRepo struct {
	mu             sync.Mutex
	deleted        bool
	deleteErr      error
	createdID      uint
	toggleCalls    int
	updateCalls    int
	createdCalls   int
	nameExists     bool
	nameExistsErr  error
	deleteObserved chan struct{}
}

func (r *applicationTaskRepo) CreateTask(_ context.Context, value *TimedTask) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.createdCalls++
	if value.ID == 0 {
		value.ID = r.createdID
	}
	return nil
}
func (r *applicationTaskRepo) UpdateTask(context.Context, *TimedTask) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.updateCalls++
	return nil
}
func (r *applicationTaskRepo) DeleteTask(context.Context, uint) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	if r.deleteErr != nil {
		return r.deleteErr
	}
	r.deleted = true
	if r.deleteObserved != nil {
		close(r.deleteObserved)
	}
	return nil
}
func (r *applicationTaskRepo) FindTask(context.Context, uint) (*TimedTask, error) {
	return nil, errors.New("not implemented")
}
func (r *applicationTaskRepo) ListTasks(context.Context, int, int, *TimedTask) ([]*TimedTask, int64, error) {
	return nil, 0, nil
}
func (r *applicationTaskRepo) ToggleTask(context.Context, uint, bool) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.toggleCalls++
	return nil
}
func (r *applicationTaskRepo) RecordTaskLog(context.Context, *TimedTaskLog) error { return nil }
func (r *applicationTaskRepo) ListTaskLogs(context.Context, int, int, uint, string) ([]*TimedTaskLog, int64, error) {
	return nil, 0, nil
}
func (r *applicationTaskRepo) CleanupTaskLogs(context.Context) error { return nil }
func (r *applicationTaskRepo) TaskNameExists(context.Context, string, uint) (bool, error) {
	return r.nameExists, r.nameExistsErr
}

type applicationTaskRuntime struct {
	mu                  sync.Mutex
	scheduleErr         error
	reloadErr           error
	scheduleCalls       int
	reloadCalls         int
	removeCalls         int
	removeBeforeDelete  bool
	repo                *applicationTaskRepo
	reloadContextActive bool
}

func (r *applicationTaskRuntime) ScheduleID(context.Context, uint) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.scheduleCalls++
	return r.scheduleErr
}
func (r *applicationTaskRuntime) Remove(uint) {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.removeCalls++
	if r.repo != nil {
		r.repo.mu.Lock()
		r.removeBeforeDelete = !r.repo.deleted
		r.repo.mu.Unlock()
	}
}
func (r *applicationTaskRuntime) TriggerID(context.Context, uint) error { return nil }
func (r *applicationTaskRuntime) NextRuns() map[uint]time.Time          { return nil }
func (r *applicationTaskRuntime) Reload(ctx context.Context) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.reloadCalls++
	r.reloadContextActive = ctx.Err() == nil
	return r.reloadErr
}
func (r *applicationTaskRuntime) Subscribe(uint) chan []byte    { return make(chan []byte) }
func (r *applicationTaskRuntime) Unsubscribe(uint, chan []byte) {}

func validApplicationTask(id uint) *TimedTask {
	const methodName = "biz-test-application-task"
	RegisterTaskMethod(methodName, "test", func(context.Context, json.RawMessage) error { return nil })
	return &TimedTask{ID: id, Name: "test", Spec: "0 0 * * *", ExecutorType: TaskExecutorMethod, MethodName: methodName, Enabled: true}
}

func TestTaskApplicationRepairsRuntimeAfterScheduleFailure(t *testing.T) {
	repo := &applicationTaskRepo{createdID: 41}
	runtime := &applicationTaskRuntime{scheduleErr: errors.New("schedule failed")}
	uc := NewTaskApplicationUsecase(NewTaskUsecase(repo), runtime)

	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	if err := uc.Create(ctx, validApplicationTask(0)); err != nil {
		t.Fatalf("Create() error = %v, want successful reload compensation", err)
	}
	if runtime.scheduleCalls != 1 || runtime.reloadCalls != 1 {
		t.Fatalf("runtime calls = schedule:%d reload:%d, want 1 and 1", runtime.scheduleCalls, runtime.reloadCalls)
	}
	if !runtime.reloadContextActive {
		t.Fatal("reload compensation inherited the canceled request context")
	}
}

func TestTaskApplicationReturnsScheduleErrorWhenRepairFails(t *testing.T) {
	repo := &applicationTaskRepo{createdID: 42}
	runtime := &applicationTaskRuntime{scheduleErr: errors.New("schedule failed"), reloadErr: errors.New("reload failed")}
	uc := NewTaskApplicationUsecase(NewTaskUsecase(repo), runtime)

	err := uc.Create(context.Background(), validApplicationTask(0))
	if !errors.Is(err, ErrTaskSchedule) {
		t.Fatalf("Create() error = %v, want ErrTaskSchedule", err)
	}
}

func TestTaskApplicationUpdateAndToggleUseRuntimeRepair(t *testing.T) {
	for _, test := range []struct {
		name string
		run  func(*TaskApplicationUsecase) error
	}{
		{name: "update", run: func(uc *TaskApplicationUsecase) error {
			return uc.Update(context.Background(), validApplicationTask(7))
		}},
		{name: "toggle", run: func(uc *TaskApplicationUsecase) error { return uc.Toggle(context.Background(), 7, true) }},
	} {
		t.Run(test.name, func(t *testing.T) {
			repo := &applicationTaskRepo{}
			runtime := &applicationTaskRuntime{scheduleErr: errors.New("schedule failed")}
			uc := NewTaskApplicationUsecase(NewTaskUsecase(repo), runtime)
			if err := test.run(uc); err != nil {
				t.Fatalf("operation error = %v", err)
			}
			if runtime.scheduleCalls != 1 || runtime.reloadCalls != 1 {
				t.Fatalf("runtime calls = schedule:%d reload:%d, want 1 and 1", runtime.scheduleCalls, runtime.reloadCalls)
			}
		})
	}
}

func TestTaskApplicationDeleteRemovesRuntimeAfterDatabase(t *testing.T) {
	repo := &applicationTaskRepo{}
	runtime := &applicationTaskRuntime{repo: repo}
	uc := NewTaskApplicationUsecase(NewTaskUsecase(repo), runtime)
	if err := uc.Delete(context.Background(), 8); err != nil {
		t.Fatalf("Delete() error = %v", err)
	}
	if runtime.removeBeforeDelete {
		t.Fatal("runtime entry was removed before the database row")
	}
	if runtime.removeCalls != 1 {
		t.Fatalf("Remove() calls = %d, want 1", runtime.removeCalls)
	}
}

func TestTaskApplicationDeleteKeepsRuntimeWhenDatabaseFails(t *testing.T) {
	repo := &applicationTaskRepo{deleteErr: errors.New("delete failed")}
	runtime := &applicationTaskRuntime{repo: repo}
	uc := NewTaskApplicationUsecase(NewTaskUsecase(repo), runtime)
	if err := uc.Delete(context.Background(), 8); err == nil {
		t.Fatal("Delete() error = nil, want database error")
	}
	if runtime.removeCalls != 0 {
		t.Fatalf("Remove() calls = %d, want 0", runtime.removeCalls)
	}
}
