package forcedstatus

import (
	"fmt"
	"testing"

	"github.com/fsnotify/fsnotify"
	. "github.com/smartystreets/goconvey/convey"
)

func newTestFileWatcher() *testFileWatcher {
	return &testFileWatcher{
		closeError: nil,
		hasClosed:  false,
		addedNames: []string{},
		events:     make(chan fsnotify.Event),
		errors:     make(chan error, 100),
	}
}

type testEvent struct {
	scope   string
	healthy bool
	reason  string
	op      fsnotify.Op
}

type testFileWatcher struct {
	closeError error
	hasClosed  bool

	addedNames []string
	events     chan fsnotify.Event
	errors     chan error
}

func (fw *testFileWatcher) Close() error {
	fw.hasClosed = true
	close(fw.events)
	close(fw.errors)
	return fw.closeError
}

func (fw *testFileWatcher) Add(name string) error {
	fw.addedNames = append(fw.addedNames, name)
	return nil
}

func (fw *testFileWatcher) Events() chan fsnotify.Event {
	return fw.events
}

func (fw *testFileWatcher) Errors() chan error {
	return fw.errors
}

func newTestReasonService() *testReasonService {
	return &testReasonService{
		healthyReasons:   make(map[string]string),
		unhealthyReasons: make(map[string]string),
	}
}

type testReasonService struct {
	healthyReasons   map[string]string
	unhealthyReasons map[string]string
}

func (rs *testReasonService) GetReason(scope string, healthy bool) string {
	if healthy {
		return rs.healthyReasons[scope]
	}

	return rs.unhealthyReasons[scope]
}

func (rs *testReasonService) Cleanup(scope string, healthy bool) error {
	if healthy {
		delete(rs.healthyReasons, scope)
	} else {
		delete(rs.unhealthyReasons, scope)
	}
	return nil
}

func (rs *testReasonService) MkdirIfNecessary(scope string) error { return nil }

func TestFileStatusWatcher(t *testing.T) {
	Convey("New File Status Watcher GetStatus", t, func(c C) {
		reason := newTestReasonService()
		fileWatcher := newTestFileWatcher()

		expectedWatcherReasons := make(map[string]*administrativeStatus)
		expectedWatcherReasons["5432"] = nil
		expectedWatcherReasons["6532"] = nil

		Convey("Test initial state", func(c C) {
			Convey("No initial statuses", func(c C) {

			})

			Convey("One port initially healthy", func(c C) {
				expectedWatcherReasons["5432"] = &administrativeStatus{
					healthy:      true,
					healthReason: "Forced healthy",
				}
				reason.healthyReasons["5432"] = "Forced healthy"
			})

			Convey("One port unhealthy & one port healthy", func(c C) {
				expectedWatcherReasons["5432"] = &administrativeStatus{
					healthy:      false,
					healthReason: "This port is bad",
				}
				reason.unhealthyReasons["5432"] = "This port is bad"

				expectedWatcherReasons["6532"] = &administrativeStatus{
					healthy:      true,
					healthReason: "This port is good",
				}
				reason.healthyReasons["6532"] = "This port is good"
			})

			Convey("All ports marked unhealthy, no overrides", func(c C) {
				reason.unhealthyReasons["all"] = "All ports are bad"

				expectedWatcherReasons["5432"] = &administrativeStatus{
					healthy:      false,
					healthReason: "All ports are bad",
				}

				expectedWatcherReasons["6532"] = &administrativeStatus{
					healthy:      false,
					healthReason: "All ports are bad",
				}
			})

			Convey("All ports marked unhealthy, one override", func(c C) {
				reason.unhealthyReasons["all"] = "All ports are bad"
				reason.healthyReasons["5432"] = "Well, actually"

				expectedWatcherReasons["5432"] = &administrativeStatus{
					healthy:      true,
					healthReason: "Well, actually",
				}
				expectedWatcherReasons["6532"] = &administrativeStatus{
					healthy:      false,
					healthReason: "All ports are bad",
				}
			})
		})

		testEvents = []*testEvent{}

		Convey("Test file events", func(c C) {

			Convey("Create single port file", func(c C) {
				addEvent("5432", true, "It's healthy", fsnotify.Create)
				expectedWatcherReasons["5432"] = &administrativeStatus{
					healthy:      true,
					healthReason: "It's healthy",
				}
			})

			Convey("Create all-port file", func(c C) {
				addEvent("all", false, "It's unhealthy", fsnotify.Create)
				expectedWatcherReasons["5432"] = &administrativeStatus{
					healthy:      false,
					healthReason: "It's unhealthy",
				}
				expectedWatcherReasons["6532"] = &administrativeStatus{
					healthy:      false,
					healthReason: "It's unhealthy",
				}
			})

			Convey("Delete an all-port file", func(c C) {
				reason.healthyReasons["all"] = "All ports are good"
				addEvent("all", true, "", fsnotify.Remove)
			})

			Convey("Modify the reason in an existing single port file", func(c C) {
				reason.healthyReasons["5432"] = "Something old"
				addEvent("5432", true, "Something new", fsnotify.Write)
				expectedWatcherReasons["5432"] = &administrativeStatus{
					healthy:      true,
					healthReason: "Something new",
				}
			})

			Convey("Change health status by adding a file", func(c C) {
				reason.healthyReasons["5432"] = "It's healthy"
				addEvent("5432", false, "It's unhealthy", fsnotify.Create)
				expectedWatcherReasons["5432"] = &administrativeStatus{
					healthy:      false,
					healthReason: "It's unhealthy",
				}
			})

			Convey("Override all-port status by adding a file", func(c C) {
				reason.healthyReasons["all"] = "It's healthy"
				addEvent("5432", false, "It's unhealthy", fsnotify.Create)
				expectedWatcherReasons["5432"] = &administrativeStatus{
					healthy:      false,
					healthReason: "It's unhealthy",
				}
				expectedWatcherReasons["6532"] = &administrativeStatus{
					healthy:      true,
					healthReason: "It's healthy",
				}
			})

		})

		watcher, err := newStatusWatcherFromFileWatcher([]int{5432, 6532}, reason, fileWatcher)
		So(err, ShouldBeNil)

		//Execute test events
		for _, evt := range testEvents {
			evt.workEvent(fileWatcher, reason)
		}

		//Force a garbage test event into the channel so we can verify that the last test event
		//has completed executing
		fileWatcher.Events() <- fsnotify.Event{
			Name: "./garbage",
			Op:   fsnotify.Create,
		}

		for key := range expectedWatcherReasons {
			val := expectedWatcherReasons[key]

			So(watcher.GetStatus(key), ShouldResemble, val)
		}
	})

	Convey("New File Status Watcher Close", t, func(c C) {
		reason := newTestReasonService()
		fileWatcher := newTestFileWatcher()

		var closeError error

		Convey("No error", func(c C) {
			closeError = nil
		})

		Convey("Some error", func(c C) {
			closeError = fmt.Errorf("An error")
		})

		watcher, err := newStatusWatcherFromFileWatcher([]int{5432, 6532}, reason, fileWatcher)
		So(err, ShouldBeNil)

		So(fileWatcher.hasClosed, ShouldBeFalse)
		fileWatcher.closeError = closeError
		So(watcher.Close(), ShouldResemble, closeError)
		So(fileWatcher.hasClosed, ShouldBeTrue)
	})
}

var testEvents []*testEvent

func addEvent(scope string, healthy bool, reason string, op fsnotify.Op) {
	testEvents = append(testEvents, &testEvent{
		scope:   scope,
		healthy: healthy,
		reason:  reason,
		op:      op,
	})
}

func (te *testEvent) workEvent(fileWatcher FileWatcher, reasonService *testReasonService) {
	if te.healthy {
		reasonService.healthyReasons[te.scope] = te.reason
	} else {
		reasonService.unhealthyReasons[te.scope] = te.reason
	}

	fileWatcher.Events() <- fsnotify.Event{
		Name: fmt.Sprintf("./%s/%s", te.scope, healthString(te.healthy)),
		Op:   te.op,
	}
}
