package monitor

import (
	"code.justin.tv/video/spectre/backend"
	"code.justin.tv/video/spectre/mocks"
	"code.justin.tv/video/spectre/util"
	"encoding/json"
	"errors"
	"github.com/jmoiron/sqlx/types"
	"testing"
)

var (
	mockDb    *mocks.MonitorBackend
	mockUsher *mocks.Usher
	mockStats *mocks.Stats
)

const (
	channelId        = 1
	activePlaylistId = 10
	activeVodId      = 100
)

// shared context
type TestContext struct {
	active         bool
	activeVodIndex int
	enabled        string
	live           bool
	master         bool
	playlist       *backend.Playlist
	playlistMap    map[string][]int
	usherActive    bool
	vodIds         []int
}

func (t *TestContext) init() {
	t.vodIds = []int{100, 200, 300}
	t.playlistMap = make(map[string][]int)
	t.playlistMap["vod_ids"] = t.vodIds
	t.playlist = &backend.Playlist{
		ID:        activePlaylistId,
		ChannelID: channelId,
		Playlist:  types.JsonText(MustMarshal(t.playlistMap)),
	}
}

func initContext(t *TestContext) {
	t.init()
	isMaster = testIsMaster
	forceMaster = &t.master

	mockDb = new(mocks.MonitorBackend)
	mockUsher = new(mocks.Usher)
	mockStats = new(mocks.Stats)

	db = mockDb
	usherService = mockUsher
	util.Stats = mockStats

	spectreChannel := backend.Channel{
		ID:               channelId,
		Enabled:          t.enabled,
		Active:           t.active,
		ActivePlaylistID: activePlaylistId,
		ActiveVodIndex:   t.activeVodIndex,
	}

	spectreChannels := []backend.Channel{spectreChannel}

	mockUsher.On("LiveChannels").Return(map[int]bool{channelId: t.live}, nil)
	mockUsher.On("SpectreChannels").Return(map[int]bool{channelId: t.usherActive}, nil)
	mockDb.On("AllChannels").Return(spectreChannels, nil)
}

// Tests

func TestIsNotMaster(t *testing.T) {
	c := &TestContext{master: false}
	initContext(c)
	runOnce()
	mockUsher.AssertNotCalled(t, "LiveChannels")
}

func TestChannelEnabledNotLive(t *testing.T) {
	c := &TestContext{
		enabled:     "yes",
		live:        false,
		usherActive: false,
		active:      false,
		master:      true,
	}
	initContext(c)
	mockDb.On("GetPlaylist", channelId, activePlaylistId).Return(c.playlist, nil)
	mockUsher.On("StreamUp", channelId, activeVodId).Return(true, 200)
	mockDb.On("BatchMarkActive", []int{channelId}).Return()
	mockStats.On("IncrByUnsampled", "spectre_monitor_deltas", 1).Return()
	runOnce()
	mockDb.AssertExpectations(t)
	mockUsher.AssertExpectations(t)
}

func TestChannelDisabled(t *testing.T) {
	c := &TestContext{
		enabled:     "", // empty string means channel is disabled
		live:        false,
		usherActive: true,
		active:      true,
		master:      true,
	}
	initContext(c)
	mockUsher.On("StreamDown", channelId).Return(true, 200)
	mockDb.On("BatchMarkInactive", []int{channelId}).Return()
	mockStats.On("IncrByUnsampled", "spectre_monitor_deltas", 1).Return()
	runOnce()
	mockDb.AssertExpectations(t)
	mockUsher.AssertExpectations(t)
}

func TestChannelLive(t *testing.T) {
	c := &TestContext{
		enabled:     "yes",
		live:        true,
		usherActive: false,
		active:      false,
		master:      true,
	}
	initContext(c)
	mockStats.On("IncrByUnsampled", "spectre_monitor_deltas", 0).Return()
	runOnce()
	mockDb.AssertExpectations(t)
	mockUsher.AssertExpectations(t)
	mockUsher.AssertNotCalled(t, "StreamUp", channelId, activeVodId)
}

func TestSpectreOutOfSyncInactive(t *testing.T) {
	c := &TestContext{
		enabled:     "yes",
		live:        false,
		usherActive: true,
		active:      false,
		master:      true,
	}
	initContext(c)
	mockDb.On("BatchMarkActive", []int{channelId}).Return()
	mockStats.On("IncrByUnsampled", "spectre_monitor_deltas", 1).Return()
	runOnce()
	mockDb.AssertExpectations(t)
	mockUsher.AssertExpectations(t)
}

func TestSpectreOutOfSyncActive(t *testing.T) {
	c := &TestContext{
		enabled:     "",
		live:        false,
		usherActive: false,
		active:      true,
		master:      true,
	}
	initContext(c)
	mockDb.On("BatchMarkInactive", []int{channelId}).Return()
	mockStats.On("IncrByUnsampled", "spectre_monitor_deltas", 1).Return()
	runOnce()
	mockDb.AssertExpectations(t)
	mockUsher.AssertExpectations(t)
}

func TestStreamUpChannelNotFound(t *testing.T) {
	c := &TestContext{
		enabled:     "yes",
		live:        false,
		usherActive: false,
		active:      false,
		master:      true,
	}
	initContext(c)
	mockDb.On("GetPlaylist", channelId, activePlaylistId).Return(c.playlist, nil)
	mockDb.On("UpdateChannel", backend.ChannelParams{ID: channelId, Enabled: false}).Return(nil)
	mockUsher.On("StreamUp", channelId, activeVodId).Return(false, 404)
	mockStats.On("IncrByUnsampled", "spectre_monitor_deltas", 0).Return()
	runOnce()
	mockDb.AssertExpectations(t)
	mockUsher.AssertExpectations(t)
}

func TestStreamUpFailed(t *testing.T) {
	c := &TestContext{
		enabled:     "yes",
		live:        false,
		usherActive: false,
		active:      true,
		master:      true,
	}
	initContext(c)
	mockDb.On("GetPlaylist", channelId, activePlaylistId).Return(c.playlist, nil)
	mockUsher.On("StreamUp", channelId, activeVodId).Return(false, 500)
	mockDb.On("BatchMarkInactive", []int{channelId}).Return()
	mockStats.On("IncrByUnsampled", "spectre_monitor_deltas", 1).Return()
	runOnce()
	mockDb.AssertExpectations(t)
	mockUsher.AssertExpectations(t)
}

func TestStreamDownFailed(t *testing.T) {
	c := &TestContext{
		enabled:     "yes",
		live:        true,
		usherActive: true,
		active:      false,
		master:      true,
	}
	initContext(c)
	mockUsher.On("StreamDown", channelId).Return(false, 500)
	mockDb.On("BatchMarkActive", []int{channelId}).Return()
	mockStats.On("IncrByUnsampled", "spectre_monitor_deltas", 1).Return()
	runOnce()
	mockDb.AssertExpectations(t)
	mockUsher.AssertExpectations(t)
}

func TestGetPlaylistFailed(t *testing.T) {
	c := &TestContext{
		enabled:     "yes",
		live:        false,
		usherActive: false,
		active:      false,
		master:      true,
	}
	initContext(c)
	mockDb.On("GetPlaylist", channelId, activePlaylistId).Return(nil, errors.New("Boom!"))
	mockDb.On("UpdateChannel", backend.ChannelParams{ID: channelId, Enabled: false}).Return(nil)
	mockStats.On("IncrByUnsampled", "spectre_monitor_deltas", 0).Return()
	runOnce()
	mockDb.AssertExpectations(t)
	mockUsher.AssertExpectations(t)
}

func TestPlaylistEmpty(t *testing.T) {
	c := &TestContext{
		enabled:     "yes",
		live:        false,
		usherActive: false,
		active:      false,
		master:      true,
	}
	initContext(c)
	c.playlistMap["vod_ids"] = []int{}
	c.playlist.Playlist = types.JsonText(MustMarshal(c.playlistMap))
	mockDb.On("GetPlaylist", channelId, activePlaylistId).Return(c.playlist, nil)
	mockDb.On("UpdateChannel", backend.ChannelParams{ID: channelId, Enabled: false}).Return(nil)
	mockStats.On("IncrByUnsampled", "spectre_monitor_deltas", 0).Return()
	runOnce()
	mockDb.AssertExpectations(t)
	mockUsher.AssertExpectations(t)
}

func TestActiveVodIndexNegative(t *testing.T) {
	c := &TestContext{
		enabled:     "yes",
		live:        false,
		usherActive: false,
		active:      false,
		master:      true,
	}
	c.activeVodIndex = -1
	initContext(c)
	mockDb.On("GetPlaylist", channelId, activePlaylistId).Return(c.playlist, nil)
	mockDb.On("UpdateChannel", backend.ChannelParams{ID: channelId, Enabled: false}).Return(nil)
	mockStats.On("IncrByUnsampled", "spectre_monitor_deltas", 0).Return()
	runOnce()
	mockDb.AssertExpectations(t)
	mockUsher.AssertExpectations(t)
}

// Helper
func MustMarshal(data interface{}) []byte {
	out, err := json.Marshal(data)
	if err != nil {
		panic(err)
	}
	return out
}

func testIsMaster() bool {
	return false
}
