package scene

import (
	"errors"

	"github.com/stashapp/stash/pkg/models"
	"github.com/stashapp/stash/pkg/models/json"
	"github.com/stashapp/stash/pkg/models/jsonschema"
	"github.com/stashapp/stash/pkg/models/mocks"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"

	"testing"
	"time"
)

const (
	sceneID    = 1
	noImageID  = 2
	errImageID = 3

	studioID        = 4
	missingStudioID = 5
	errStudioID     = 6
	customFieldsID  = 7

	noTagsID  = 11
	errTagsID = 12

	noGroupsID     = 13
	errFindGroupID = 15

	noMarkersID         = 16
	errMarkersID        = 17
	errFindPrimaryTagID = 18
	errFindByMarkerID   = 19
	errCustomFieldsID   = 20
)

var (
	url        = "url"
	title      = "title"
	date       = "2001-01-01"
	dateObj, _ = models.ParseDate(date)
	rating     = 5
	organized  = true
	details    = "details"
)

var (
	studioName = "studioName"
	// galleryChecksum = "galleryChecksum"

	validGroup1  = 1
	validGroup2  = 2
	invalidGroup = 3

	group1Name = "group1Name"
	group2Name = "group2Name"

	group1Scene = 1
	group2Scene = 2
)

var names = []string{
	"name1",
	"name2",
}

var imageBytes = []byte("imageBytes")

var stashID = models.StashID{
	StashID:  "StashID",
	Endpoint: "Endpoint",
}

const (
	path        = "path"
	imageBase64 = "aW1hZ2VCeXRlcw=="
)

var (
	createTime = time.Date(2001, 01, 01, 0, 0, 0, 0, time.UTC)
	updateTime = time.Date(2002, 01, 01, 0, 0, 0, 0, time.UTC)
)

var (
	emptyCustomFields = make(map[string]interface{})
	customFields      = map[string]interface{}{
		"customField1": "customValue1",
	}
)

func createFullScene(id int) models.Scene {
	return models.Scene{
		ID:        id,
		Title:     title,
		Date:      &dateObj,
		Details:   details,
		Rating:    &rating,
		Organized: organized,
		URLs:      models.NewRelatedStrings([]string{url}),
		Files: models.NewRelatedVideoFiles([]*models.VideoFile{
			{
				BaseFile: &models.BaseFile{
					Path: path,
				},
			},
		}),
		StashIDs: models.NewRelatedStashIDs([]models.StashID{
			stashID,
		}),
		CreatedAt: createTime,
		UpdatedAt: updateTime,
	}
}

func createEmptyScene(id int) models.Scene {
	return models.Scene{
		ID: id,
		Files: models.NewRelatedVideoFiles([]*models.VideoFile{
			{
				BaseFile: &models.BaseFile{
					Path: path,
				},
			},
		}),
		URLs:      models.NewRelatedStrings([]string{}),
		StashIDs:  models.NewRelatedStashIDs([]models.StashID{}),
		CreatedAt: createTime,
		UpdatedAt: updateTime,
	}
}

func createFullJSONScene(image string, customFields map[string]interface{}) *jsonschema.Scene {
	return &jsonschema.Scene{
		Title:     title,
		Files:     []string{path},
		Date:      date,
		Details:   details,
		Rating:    rating,
		Organized: organized,
		URLs:      []string{url},
		CreatedAt: json.JSONTime{
			Time: createTime,
		},
		UpdatedAt: json.JSONTime{
			Time: updateTime,
		},
		Cover: image,
		StashIDs: []models.StashID{
			stashID,
		},
		CustomFields: customFields,
	}
}

func createEmptyJSONScene() *jsonschema.Scene {
	return &jsonschema.Scene{
		URLs:  []string{},
		Files: []string{path},
		CreatedAt: json.JSONTime{
			Time: createTime,
		},
		UpdatedAt: json.JSONTime{
			Time: updateTime,
		},
		CustomFields: emptyCustomFields,
	}
}

type basicTestScenario struct {
	input        models.Scene
	customFields map[string]interface{}
	expected     *jsonschema.Scene
	err          bool
}

var scenarios = []basicTestScenario{
	{
		createFullScene(sceneID),
		emptyCustomFields,
		createFullJSONScene(imageBase64, emptyCustomFields),
		false,
	},
	{
		createFullScene(customFieldsID),
		customFields,
		createFullJSONScene("", customFields),
		false,
	},
	{
		createEmptyScene(noImageID),
		emptyCustomFields,
		createEmptyJSONScene(),
		false,
	},
	{
		createFullScene(errImageID),
		emptyCustomFields,
		createFullJSONScene("", emptyCustomFields),
		// failure to get image should not cause an error
		false,
	},
	{
		createFullScene(errCustomFieldsID),
		customFields,
		createFullJSONScene("", customFields),
		true,
	},
}

func TestToJSON(t *testing.T) {
	db := mocks.NewDatabase()

	imageErr := errors.New("error getting image")

	db.Scene.On("GetCover", testCtx, sceneID).Return(imageBytes, nil).Once()
	db.Scene.On("GetCover", testCtx, noImageID).Return(nil, nil).Once()
	db.Scene.On("GetCover", testCtx, errImageID).Return(nil, imageErr).Once()
	db.Scene.On("GetCover", testCtx, mock.Anything).Return(nil, nil)
	db.Scene.On("GetViewDates", testCtx, mock.Anything).Return(nil, nil)
	db.Scene.On("GetODates", testCtx, mock.Anything).Return(nil, nil)
	db.Scene.On("GetCustomFields", testCtx, customFieldsID).Return(customFields, nil).Once()
	db.Scene.On("GetCustomFields", testCtx, errCustomFieldsID).Return(nil, errors.New("error getting custom fields")).Once()
	db.Scene.On("GetCustomFields", testCtx, mock.Anything).Return(emptyCustomFields, nil)

	for i, s := range scenarios {
		scene := s.input
		json, err := ToBasicJSON(testCtx, db.Scene, &scene)

		switch {
		case !s.err && err != nil:
			t.Errorf("[%d] unexpected error: %s", i, err.Error())
		case s.err && err == nil:
			t.Errorf("[%d] expected error not returned", i)
		case err != nil:
			// error case already handled, no need for assertion
		default:
			assert.Equal(t, s.expected, json, "[%d]", i)
		}
	}

	db.AssertExpectations(t)
}

func createStudioScene(studioID int) models.Scene {
	return models.Scene{
		StudioID: &studioID,
	}
}

type stringTestScenario struct {
	input    models.Scene
	expected string
	err      bool
}

var getStudioScenarios = []stringTestScenario{
	{
		createStudioScene(studioID),
		studioName,
		false,
	},
	{
		createStudioScene(missingStudioID),
		"",
		false,
	},
	{
		createStudioScene(errStudioID),
		"",
		true,
	},
}

func TestGetStudioName(t *testing.T) {
	db := mocks.NewDatabase()

	studioErr := errors.New("error getting image")

	db.Studio.On("Find", testCtx, studioID).Return(&models.Studio{
		Name: studioName,
	}, nil).Once()
	db.Studio.On("Find", testCtx, missingStudioID).Return(nil, nil).Once()
	db.Studio.On("Find", testCtx, errStudioID).Return(nil, studioErr).Once()

	for i, s := range getStudioScenarios {
		scene := s.input
		json, err := GetStudioName(testCtx, db.Studio, &scene)

		switch {
		case !s.err && err != nil:
			t.Errorf("[%d] unexpected error: %s", i, err.Error())
		case s.err && err == nil:
			t.Errorf("[%d] expected error not returned", i)
		default:
			assert.Equal(t, s.expected, json, "[%d]", i)
		}
	}

	db.AssertExpectations(t)
}

type stringSliceTestScenario struct {
	input    models.Scene
	expected []string
	err      bool
}

var getTagNamesScenarios = []stringSliceTestScenario{
	{
		createEmptyScene(sceneID),
		names,
		false,
	},
	{
		createEmptyScene(noTagsID),
		nil,
		false,
	},
	{
		createEmptyScene(errTagsID),
		nil,
		true,
	},
}

func getTags(names []string) []*models.Tag {
	var ret []*models.Tag
	for _, n := range names {
		ret = append(ret, &models.Tag{
			Name: n,
		})
	}

	return ret
}

func TestGetTagNames(t *testing.T) {
	db := mocks.NewDatabase()

	tagErr := errors.New("error getting tag")

	db.Tag.On("FindBySceneID", testCtx, sceneID).Return(getTags(names), nil).Once()
	db.Tag.On("FindBySceneID", testCtx, noTagsID).Return(nil, nil).Once()
	db.Tag.On("FindBySceneID", testCtx, errTagsID).Return(nil, tagErr).Once()

	for i, s := range getTagNamesScenarios {
		scene := s.input
		json, err := GetTagNames(testCtx, db.Tag, &scene)

		switch {
		case !s.err && err != nil:
			t.Errorf("[%d] unexpected error: %s", i, err.Error())
		case s.err && err == nil:
			t.Errorf("[%d] expected error not returned", i)
		default:
			assert.Equal(t, s.expected, json, "[%d]", i)
		}
	}

	db.AssertExpectations(t)
}

type sceneGroupsTestScenario struct {
	input    models.Scene
	expected []jsonschema.SceneGroup
	err      bool
}

var validGroups = models.NewRelatedGroups([]models.GroupsScenes{
	{
		GroupID:    validGroup1,
		SceneIndex: &group1Scene,
	},
	{
		GroupID:    validGroup2,
		SceneIndex: &group2Scene,
	},
})

var invalidGroups = models.NewRelatedGroups([]models.GroupsScenes{
	{
		GroupID:    invalidGroup,
		SceneIndex: &group1Scene,
	},
})

var getSceneGroupsJSONScenarios = []sceneGroupsTestScenario{
	{
		models.Scene{
			ID:     sceneID,
			Groups: validGroups,
		},
		[]jsonschema.SceneGroup{
			{
				GroupName:  group1Name,
				SceneIndex: group1Scene,
			},
			{
				GroupName:  group2Name,
				SceneIndex: group2Scene,
			},
		},
		false,
	},
	{
		models.Scene{
			ID:     noGroupsID,
			Groups: models.NewRelatedGroups([]models.GroupsScenes{}),
		},
		nil,
		false,
	},
	{
		models.Scene{
			ID:     errFindGroupID,
			Groups: invalidGroups,
		},
		nil,
		true,
	},
}

func TestGetSceneGroupsJSON(t *testing.T) {
	db := mocks.NewDatabase()

	groupErr := errors.New("error getting group")

	db.Group.On("Find", testCtx, validGroup1).Return(&models.Group{
		Name: group1Name,
	}, nil).Once()
	db.Group.On("Find", testCtx, validGroup2).Return(&models.Group{
		Name: group2Name,
	}, nil).Once()
	db.Group.On("Find", testCtx, invalidGroup).Return(nil, groupErr).Once()

	for i, s := range getSceneGroupsJSONScenarios {
		scene := s.input
		json, err := GetSceneGroupsJSON(testCtx, db.Group, &scene)

		switch {
		case !s.err && err != nil:
			t.Errorf("[%d] unexpected error: %s", i, err.Error())
		case s.err && err == nil:
			t.Errorf("[%d] expected error not returned", i)
		default:
			assert.Equal(t, s.expected, json, "[%d]", i)
		}
	}

	db.AssertExpectations(t)
}

const (
	validMarkerID1 = 1
	validMarkerID2 = 2

	invalidMarkerID1 = 3
	invalidMarkerID2 = 4

	validTagID1 = 1
	validTagID2 = 2

	validTagName1 = "validTagName1"
	validTagName2 = "validTagName2"

	invalidTagID = 3

	markerTitle1 = "markerTitle1"
	markerTitle2 = "markerTitle2"

	markerSeconds1 = 1.0
	markerSeconds2 = 2.3

	markerSeconds1Str = "1.0"
	markerSeconds2Str = "2.3"
)

type sceneMarkersTestScenario struct {
	input    models.Scene
	expected []jsonschema.SceneMarker
	err      bool
}

var getSceneMarkersJSONScenarios = []sceneMarkersTestScenario{
	{
		createEmptyScene(sceneID),
		[]jsonschema.SceneMarker{
			{
				Title:      markerTitle1,
				PrimaryTag: validTagName1,
				Seconds:    markerSeconds1Str,
				Tags: []string{
					validTagName1,
					validTagName2,
				},
				CreatedAt: json.JSONTime{
					Time: createTime,
				},
				UpdatedAt: json.JSONTime{
					Time: updateTime,
				},
			},
			{
				Title:      markerTitle2,
				PrimaryTag: validTagName2,
				Seconds:    markerSeconds2Str,
				Tags: []string{
					validTagName2,
				},
				CreatedAt: json.JSONTime{
					Time: createTime,
				},
				UpdatedAt: json.JSONTime{
					Time: updateTime,
				},
			},
		},
		false,
	},
	{
		createEmptyScene(noMarkersID),
		nil,
		false,
	},
	{
		createEmptyScene(errMarkersID),
		nil,
		true,
	},
	{
		createEmptyScene(errFindPrimaryTagID),
		nil,
		true,
	},
	{
		createEmptyScene(errFindByMarkerID),
		nil,
		true,
	},
}

var validMarkers = []*models.SceneMarker{
	{
		ID:           validMarkerID1,
		Title:        markerTitle1,
		PrimaryTagID: validTagID1,
		Seconds:      markerSeconds1,
		CreatedAt:    createTime,
		UpdatedAt:    updateTime,
	},
	{
		ID:           validMarkerID2,
		Title:        markerTitle2,
		PrimaryTagID: validTagID2,
		Seconds:      markerSeconds2,
		CreatedAt:    createTime,
		UpdatedAt:    updateTime,
	},
}

var invalidMarkers1 = []*models.SceneMarker{
	{
		ID:           invalidMarkerID1,
		PrimaryTagID: invalidTagID,
	},
}

var invalidMarkers2 = []*models.SceneMarker{
	{
		ID:           invalidMarkerID2,
		PrimaryTagID: validTagID1,
	},
}

func TestGetSceneMarkersJSON(t *testing.T) {
	db := mocks.NewDatabase()

	markersErr := errors.New("error getting scene markers")
	tagErr := errors.New("error getting tags")

	db.SceneMarker.On("FindBySceneID", testCtx, sceneID).Return(validMarkers, nil).Once()
	db.SceneMarker.On("FindBySceneID", testCtx, noMarkersID).Return(nil, nil).Once()
	db.SceneMarker.On("FindBySceneID", testCtx, errMarkersID).Return(nil, markersErr).Once()
	db.SceneMarker.On("FindBySceneID", testCtx, errFindPrimaryTagID).Return(invalidMarkers1, nil).Once()
	db.SceneMarker.On("FindBySceneID", testCtx, errFindByMarkerID).Return(invalidMarkers2, nil).Once()

	db.Tag.On("Find", testCtx, validTagID1).Return(&models.Tag{
		Name: validTagName1,
	}, nil)
	db.Tag.On("Find", testCtx, validTagID2).Return(&models.Tag{
		Name: validTagName2,
	}, nil)
	db.Tag.On("Find", testCtx, invalidTagID).Return(nil, tagErr)

	db.Tag.On("FindBySceneMarkerID", testCtx, validMarkerID1).Return([]*models.Tag{
		{
			Name: validTagName1,
		},
		{
			Name: validTagName2,
		},
	}, nil)
	db.Tag.On("FindBySceneMarkerID", testCtx, validMarkerID2).Return([]*models.Tag{
		{
			Name: validTagName2,
		},
	}, nil)
	db.Tag.On("FindBySceneMarkerID", testCtx, invalidMarkerID2).Return(nil, tagErr).Once()

	for i, s := range getSceneMarkersJSONScenarios {
		scene := s.input
		json, err := GetSceneMarkersJSON(testCtx, db.SceneMarker, db.Tag, &scene)

		switch {
		case !s.err && err != nil:
			t.Errorf("[%d] unexpected error: %s", i, err.Error())
		case s.err && err == nil:
			t.Errorf("[%d] expected error not returned", i)
		default:
			assert.Equal(t, s.expected, json, "[%d]", i)
		}
	}

	db.AssertExpectations(t)
}
