package middleware_test

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/mock"
	"github.com/influxdata/influxdb/v2/notification/check"
	"github.com/influxdata/influxdb/v2/notification/rule"
	"github.com/influxdata/influxdb/v2/task/backend/middleware"
)

type pipingCoordinator struct {
	err             error
	taskCreatedPipe chan *influxdb.Task
	taskUpdatedPipe chan *influxdb.Task
	taskDeletedPipe chan influxdb.ID
}

func (p *pipingCoordinator) taskCreatedChan() <-chan *influxdb.Task {
	if p.taskCreatedPipe == nil {
		p.taskCreatedPipe = make(chan *influxdb.Task, 1)
	}
	return p.taskCreatedPipe
}
func (p *pipingCoordinator) taskUpdatedChan() <-chan *influxdb.Task {
	if p.taskUpdatedPipe == nil {
		p.taskUpdatedPipe = make(chan *influxdb.Task, 1)
	}
	return p.taskUpdatedPipe
}
func (p *pipingCoordinator) taskDeletedChan() <-chan influxdb.ID {
	if p.taskDeletedPipe == nil {
		p.taskDeletedPipe = make(chan influxdb.ID, 1)
	}
	return p.taskDeletedPipe
}

func (p *pipingCoordinator) TaskCreated(_ context.Context, t *influxdb.Task) error {
	if p.taskCreatedPipe != nil {
		p.taskCreatedPipe <- t
	}
	return p.err
}
func (p *pipingCoordinator) TaskUpdated(_ context.Context, from, to *influxdb.Task) error {
	if p.taskUpdatedPipe != nil {
		p.taskUpdatedPipe <- to
	}
	return p.err
}
func (p *pipingCoordinator) TaskDeleted(_ context.Context, id influxdb.ID) error {
	if p.taskDeletedPipe != nil {
		p.taskDeletedPipe <- id
	}
	return p.err
}
func (p *pipingCoordinator) RunCancelled(ctx context.Context, runID influxdb.ID) error {
	return p.err
}
func (p *pipingCoordinator) RunRetried(ctx context.Context, task *influxdb.Task, run *influxdb.Run) error {
	return p.err
}
func (p *pipingCoordinator) RunForced(ctx context.Context, task *influxdb.Task, run *influxdb.Run) error {
	return p.err
}

type mockedSvc struct {
	taskSvc           *mock.TaskService
	checkSvc          *mock.CheckService
	notificationSvc   *mock.NotificationRuleStore
	pipingCoordinator *pipingCoordinator
}

func newMockServices() mockedSvc {
	return mockedSvc{
		taskSvc: &mock.TaskService{
			FindTaskByIDFn: func(_ context.Context, id influxdb.ID) (*influxdb.Task, error) { return &influxdb.Task{ID: id}, nil },
			CreateTaskFn:   func(context.Context, influxdb.TaskCreate) (*influxdb.Task, error) { return &influxdb.Task{ID: 1}, nil },
			UpdateTaskFn: func(_ context.Context, id influxdb.ID, _ influxdb.TaskUpdate) (*influxdb.Task, error) {
				return &influxdb.Task{ID: id}, nil
			},
			DeleteTaskFn: func(context.Context, influxdb.ID) error { return nil },
		},
		checkSvc: &mock.CheckService{
			FindCheckByIDFn: func(_ context.Context, id influxdb.ID) (influxdb.Check, error) {
				c := &check.Deadman{}
				c.SetID(id)
				return c, nil
			},
			CreateCheckFn: func(context.Context, influxdb.CheckCreate, influxdb.ID) error { return nil },
			UpdateCheckFn: func(_ context.Context, _ influxdb.ID, c influxdb.CheckCreate) (influxdb.Check, error) { return c, nil },
			PatchCheckFn: func(_ context.Context, id influxdb.ID, _ influxdb.CheckUpdate) (influxdb.Check, error) {
				c := &check.Deadman{}
				c.SetID(id)
				return c, nil
			},
			DeleteCheckFn: func(context.Context, influxdb.ID) error { return nil },
		},
		notificationSvc: &mock.NotificationRuleStore{
			FindNotificationRuleByIDF: func(_ context.Context, id influxdb.ID) (influxdb.NotificationRule, error) {
				c := &rule.HTTP{}
				c.SetID(id)
				return c, nil
			},
			CreateNotificationRuleF: func(context.Context, influxdb.NotificationRuleCreate, influxdb.ID) error { return nil },
			UpdateNotificationRuleF: func(_ context.Context, _ influxdb.ID, c influxdb.NotificationRuleCreate, _ influxdb.ID) (influxdb.NotificationRule, error) {
				return c, nil
			},
			PatchNotificationRuleF: func(_ context.Context, id influxdb.ID, _ influxdb.NotificationRuleUpdate) (influxdb.NotificationRule, error) {
				c := &rule.HTTP{}
				c.SetID(id)
				return c, nil
			},
			DeleteNotificationRuleF: func(context.Context, influxdb.ID) error { return nil },
		},
		pipingCoordinator: &pipingCoordinator{},
	}
}

func newCheckSvcStack() (mockedSvc, *middleware.CoordinatingCheckService) {
	msvcs := newMockServices()
	return msvcs, middleware.NewCheckService(msvcs.checkSvc, msvcs.taskSvc, msvcs.pipingCoordinator)
}

func TestCheckCreate(t *testing.T) {
	mocks, checkService := newCheckSvcStack()
	ch := mocks.pipingCoordinator.taskCreatedChan()

	check := &check.Deadman{}
	check.SetTaskID(4)

	cc := influxdb.CheckCreate{
		Check:  check,
		Status: influxdb.Active,
	}

	err := checkService.CreateCheck(context.Background(), cc, 1)
	if err != nil {
		t.Fatal(err)
	}

	select {
	case task := <-ch:
		if task.ID != check.GetTaskID() {
			t.Fatalf("task sent to coordinator doesn't match expected")
		}
	default:
		t.Fatal("didn't receive task")
	}

	mocks.pipingCoordinator.err = fmt.Errorf("bad")
	mocks.checkSvc.DeleteCheckFn = func(context.Context, influxdb.ID) error { return fmt.Errorf("AARGH") }

	err = checkService.CreateCheck(context.Background(), cc, 1)
	if err.Error() != "schedule task failed: bad\n\tcleanup also failed: AARGH" {
		t.Fatal(err)
	}
}

func TestCheckUpdateFromInactive(t *testing.T) {
	mocks, checkService := newCheckSvcStack()
	latest := time.Now().UTC()
	checkService.Now = func() time.Time {
		return latest
	}
	ch := mocks.pipingCoordinator.taskUpdatedChan()

	mocks.checkSvc.UpdateCheckFn = func(_ context.Context, _ influxdb.ID, c influxdb.CheckCreate) (influxdb.Check, error) {
		c.SetTaskID(10)
		c.SetUpdatedAt(latest.Add(-20 * time.Hour))
		return c, nil
	}

	mocks.checkSvc.PatchCheckFn = func(_ context.Context, _ influxdb.ID, c influxdb.CheckUpdate) (influxdb.Check, error) {
		ic := &check.Deadman{}
		ic.SetTaskID(10)
		ic.SetUpdatedAt(latest.Add(-20 * time.Hour))
		return ic, nil
	}

	mocks.checkSvc.FindCheckByIDFn = func(_ context.Context, id influxdb.ID) (influxdb.Check, error) {
		c := &check.Deadman{}
		c.SetID(id)
		c.SetTaskID(1)
		return c, nil
	}

	mocks.taskSvc.FindTaskByIDFn = func(_ context.Context, id influxdb.ID) (*influxdb.Task, error) {
		if id == 1 {
			return &influxdb.Task{ID: id, Status: string(influxdb.TaskInactive)}, nil
		} else if id == 10 {
			return &influxdb.Task{ID: id, Status: string(influxdb.TaskActive)}, nil
		}
		return &influxdb.Task{ID: id}, nil
	}

	deadman := &check.Deadman{}
	deadman.SetTaskID(10)

	cc := influxdb.CheckCreate{
		Check:  deadman,
		Status: influxdb.Active,
	}

	thecheck, err := checkService.UpdateCheck(context.Background(), 1, cc)
	if err != nil {
		t.Fatal(err)
	}
	select {
	case task := <-ch:
		if task.ID != thecheck.GetTaskID() {
			t.Fatalf("task sent to coordinator doesn't match expected")
		}
		if task.LatestCompleted != latest {
			t.Fatalf("update returned incorrect LatestCompleted, expected %s got %s, or ", latest.Format(time.RFC3339), task.LatestCompleted)
		}
	default:
		t.Fatal("didn't receive task")
	}

	action := influxdb.Active
	thecheck, err = checkService.PatchCheck(context.Background(), 1, influxdb.CheckUpdate{Status: &action})
	if err != nil {
		t.Fatal(err)
	}
	select {
	case task := <-ch:
		if task.ID != thecheck.GetTaskID() {
			t.Fatalf("task sent to coordinator doesn't match expected")
		}
		if task.LatestCompleted != latest {
			t.Fatalf("update returned incorrect LatestCompleted, expected %s got %s, or ", latest.Format(time.RFC3339), task.LatestCompleted)
		}
	default:
		t.Fatal("didn't receive task")
	}

}

func TestCheckUpdate(t *testing.T) {
	mocks, checkService := newCheckSvcStack()
	ch := mocks.pipingCoordinator.taskUpdatedChan()

	mocks.checkSvc.UpdateCheckFn = func(_ context.Context, _ influxdb.ID, c influxdb.CheckCreate) (influxdb.Check, error) {
		c.SetTaskID(10)
		return c, nil
	}

	deadman := &check.Deadman{}
	deadman.SetTaskID(4)

	cc := influxdb.CheckCreate{
		Check:  deadman,
		Status: influxdb.Active,
	}

	check, err := checkService.UpdateCheck(context.Background(), 1, cc)
	if err != nil {
		t.Fatal(err)
	}

	select {
	case task := <-ch:
		if task.ID != check.GetTaskID() {
			t.Fatalf("task sent to coordinator doesn't match expected")
		}
	default:
		t.Fatal("didn't receive task")
	}
}

func TestCheckPatch(t *testing.T) {
	mocks, checkService := newCheckSvcStack()
	ch := mocks.pipingCoordinator.taskUpdatedChan()

	deadman := &check.Deadman{}
	deadman.SetTaskID(4)

	mocks.checkSvc.PatchCheckFn = func(context.Context, influxdb.ID, influxdb.CheckUpdate) (influxdb.Check, error) {
		return deadman, nil
	}

	check, err := checkService.PatchCheck(context.Background(), 1, influxdb.CheckUpdate{})
	if err != nil {
		t.Fatal(err)
	}

	select {
	case task := <-ch:
		if task.ID != check.GetTaskID() {
			t.Fatalf("task sent to coordinator doesn't match expected")
		}
	default:
		t.Fatal("didn't receive task")
	}
}

func TestCheckDelete(t *testing.T) {
	mocks, checkService := newCheckSvcStack()
	ch := mocks.pipingCoordinator.taskDeletedChan()

	mocks.checkSvc.FindCheckByIDFn = func(_ context.Context, id influxdb.ID) (influxdb.Check, error) {
		c := &check.Deadman{}
		c.SetID(id)
		c.SetTaskID(21)
		return c, nil
	}

	err := checkService.DeleteCheck(context.Background(), 1)
	if err != nil {
		t.Fatal(err)
	}

	select {
	case id := <-ch:
		if id != influxdb.ID(21) {
			t.Fatalf("task sent to coordinator doesn't match expected")
		}
	default:
		t.Fatal("didn't receive task")
	}
}
