package state

import (
	"testing"
	"time"

	"code.justin.tv/eventbus/controlplane/infrastructure/validation"
	itemMock "code.justin.tv/eventbus/controlplane/infrastructure/validation/mocks"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func newMockItem(id string) *itemMock.Item {
	i := &itemMock.Item{}
	i.On("ID").Return(id)
	i.On("Attributes").Return([]*validation.ItemAttribute{})
	i.On("Type").Return("CoolType")
	return i
}

type mockStateChanger struct {
	opens        map[string]int
	resolves     map[string]int
	stateChanges map[string]int
	renotifies   map[string]int
}

func newMockStateChanger() *mockStateChanger {
	return &mockStateChanger{
		opens:        make(map[string]int),
		resolves:     make(map[string]int),
		stateChanges: make(map[string]int),
		renotifies:   make(map[string]int),
	}
}

func (m *mockStateChanger) Open(r *validation.Report) error {
	m.opens[r.Item.ID()] += 1
	return nil
}
func (m *mockStateChanger) ChangeStatus(r1 *validation.Report, r2 *validation.Report) error {
	m.stateChanges[r1.Item.ID()] += 1
	return nil
}
func (m *mockStateChanger) Renotify(r *validation.Report) error {
	m.renotifies[r.Item.ID()] += 1
	return nil
}
func (m *mockStateChanger) Resolve(oldR, newR *validation.Report) error {
	m.resolves[oldR.Item.ID()] += 1
	return nil
}

func newStateTrackerWithMocks(dedupeInterval time.Duration, minOccurrances int) (*Tracker, *mockStateChanger) {
	m := newMockStateChanger()
	return NewTracker(m, dedupeInterval, minOccurrances), m
}

func TestStateChanger(t *testing.T) {
	t.Run("Happy path", func(t *testing.T) {
		tracker, m := newStateTrackerWithMocks(time.Hour, 1)
		item := newMockItem("foobar")
		err := tracker.Handle(validation.Ok(item))

		t.Run("does not error", func(t *testing.T) {
			require.NoError(t, err)
		})

		t.Run("does not call any state change methods", func(t *testing.T) {
			assert.Equal(t, 0, len(m.opens))
			assert.Equal(t, 0, len(m.resolves))
			assert.Equal(t, 0, len(m.stateChanges))
			assert.Equal(t, 0, len(m.renotifies))
		})

		t.Run("does not get registered in occurance counting", func(t *testing.T) {
			assert.Equal(t, 0, len(tracker.occuranceCounter))
		})
	})

	t.Run("Minimum occurrances", func(t *testing.T) {
		requiredOccurances := 5
		tracker, m := newStateTrackerWithMocks(time.Hour, requiredOccurances)
		item := newMockItem("foobar")

		t.Run("Only opens after enough consecutive non OK reports", func(t *testing.T) {
			for i := 0; i < requiredOccurances-1; i++ {
				err := tracker.Handle(validation.Warn(item, "warn msg"))
				require.NoError(t, err)
			}

			assert.Equal(t, 0, m.opens[item.ID()])

			err := tracker.Handle(validation.Warn(item, "warn msg"))
			require.NoError(t, err)

			assert.Equal(t, 1, m.opens[item.ID()])
		})

		t.Run("OK reports wipe existing occurrance count", func(t *testing.T) {
			err := tracker.Handle(validation.Ok(item))
			require.NoError(t, err)
			assert.Equal(t, 0, tracker.occuranceCounter[item.ID()])
		})
	})

	t.Run("Open incident", func(t *testing.T) {
		tracker, m := newStateTrackerWithMocks(time.Hour, 1)
		item := newMockItem("foobar")

		report := validation.Error(item, "error msg")
		err := tracker.Handle(report)
		require.NoError(t, err)

		t.Run("Open method is called", func(t *testing.T) {
			assert.Equal(t, 1, m.opens[item.ID()])
		})

		t.Run("internal state is tracking the report", func(t *testing.T) {
			assert.Equal(t, report, tracker.state[item.ID()])
		})
	})

	t.Run("Renotification", func(t *testing.T) {
		tracker, m := newStateTrackerWithMocks(5*time.Second, 1)
		item := newMockItem("garply")

		oldReport := validation.Error(item, "error msg")
		oldReport.Timestamp = time.Now().Add(-2 * time.Hour)

		newReport := validation.Error(item, "error msg")
		newReport.Timestamp = time.Now()

		err := tracker.Handle(oldReport)
		require.NoError(t, err)
		require.Equal(t, 1, m.opens[item.ID()])

		t.Run("deduplication when not enough time elapses", func(t *testing.T) {
			err := tracker.Handle(oldReport)
			require.NoError(t, err)
			assert.Equal(t, 0, m.renotifies[item.ID()])
		})

		t.Run("renotification when enough time elapses", func(t *testing.T) {
			err := tracker.Handle(newReport)
			require.NoError(t, err)
			assert.Equal(t, 1, m.renotifies[item.ID()])
			assert.Equal(t, newReport, tracker.state[item.ID()])
		})
	})

	t.Run("ChangeStatus", func(t *testing.T) {
		tracker, m := newStateTrackerWithMocks(time.Hour, 1)
		reportTwoStatuses := func(t *testing.T, itemName string, firstStatus validation.Status, secondStatus validation.Status) {
			firstReport := &validation.Report{
				Item:      newMockItem(itemName),
				Status:    firstStatus,
				Timestamp: time.Now().Add(-5 * time.Minute),
			}
			secondReport := &validation.Report{
				Item:      newMockItem(itemName),
				Status:    secondStatus,
				Timestamp: time.Now(),
			}

			err := tracker.Handle(firstReport)
			require.NoError(t, err)

			err = tracker.Handle(secondReport)
			require.NoError(t, err)
		}

		t.Run("WARN to ERROR", func(t *testing.T) {
			reportTwoStatuses(t, "foobar", validation.StatusWarn, validation.StatusError)

			t.Run("ChangeStatus is called", func(t *testing.T) {
				assert.Equal(t, 1, m.stateChanges["foobar"])
			})

			t.Run("internal state holds new report status", func(t *testing.T) {
				assert.Equal(t, validation.StatusError, tracker.state["foobar"].Status)
			})
		})

		t.Run("ERROR to WARN", func(t *testing.T) {
			reportTwoStatuses(t, "garply", validation.StatusError, validation.StatusWarn)

			t.Run("ChangeStatus is called", func(t *testing.T) {
				assert.Equal(t, 1, m.stateChanges["garply"])
			})

			t.Run("internal state holds new report status", func(t *testing.T) {
				assert.Equal(t, validation.StatusWarn, tracker.state["garply"].Status)
			})
		})

		t.Run("VALIDATION_ERROR to ERROR", func(t *testing.T) {
			reportTwoStatuses(t, "wubwubwub", validation.StatusValidationError, validation.StatusError)

			t.Run("ChangeStatus is called", func(t *testing.T) {
				assert.Equal(t, 1, m.stateChanges["wubwubwub"])
			})

			t.Run("internal state holds new report status", func(t *testing.T) {
				assert.Equal(t, validation.StatusError, tracker.state["wubwubwub"].Status)
			})
		})

		t.Run("ERROR to VALIDATION_ERROR", func(t *testing.T) {
			reportTwoStatuses(t, "coolthing", validation.StatusError, validation.StatusValidationError)

			t.Run("ChangeStatus is called", func(t *testing.T) {
				assert.Equal(t, 1, m.stateChanges["coolthing"])
			})

			t.Run("internal state holds new report status", func(t *testing.T) {
				assert.Equal(t, validation.StatusValidationError, tracker.state["coolthing"].Status)
			})
		})
	})

}
