// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package timeline_test

import (
	"context"
	"sync"
	"testing"

	"github.com/stretchr/testify/suite"
	"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
	"github.com/superseriousbusiness/gotosocial/internal/id"
	"github.com/superseriousbusiness/gotosocial/internal/timeline"
)

type GetTestSuite struct {
	TimelineStandardTestSuite
}

func (suite *GetTestSuite) checkStatuses(statuses []timeline.Preparable, maxID string, minID string, expectedLength int) {
	if l := len(statuses); l != expectedLength {
		suite.FailNow("", "expected %d statuses in slice, got %d", expectedLength, l)
	} else if l == 0 {
		// Can't test empty slice.
		return
	}

	// Check ordering + bounds of statuses.
	highest := statuses[0].GetID()
	for _, status := range statuses {
		id := status.GetID()

		if id >= maxID {
			suite.FailNow("", "%s greater than maxID %s", id, maxID)
		}

		if id <= minID {
			suite.FailNow("", "%s smaller than minID %s", id, minID)
		}

		if id > highest {
			suite.FailNow("", "statuses in slice were not ordered highest -> lowest ID")
		}

		highest = id
	}
}

func (suite *GetTestSuite) emptyAccountFollows(ctx context.Context, accountID string) {
	// Get all of account's follows.
	follows, err := suite.state.DB.GetAccountFollows(
		gtscontext.SetBarebones(ctx),
		accountID,
		nil, // select all
	)
	if err != nil {
		suite.FailNow(err.Error())
	}

	// Remove each follow.
	for _, follow := range follows {
		if err := suite.state.DB.DeleteFollowByID(ctx, follow.ID); err != nil {
			suite.FailNow(err.Error())
		}
	}

	// Ensure no follows left.
	follows, err = suite.state.DB.GetAccountFollows(
		gtscontext.SetBarebones(ctx),
		accountID,
		nil, // select all
	)
	if err != nil {
		suite.FailNow(err.Error())
	}
	if len(follows) != 0 {
		suite.FailNow("follows should be empty")
	}
}

func (suite *GetTestSuite) emptyAccountStatuses(ctx context.Context, accountID string) {
	// Get all of account's statuses.
	statuses, err := suite.state.DB.GetAccountStatuses(
		ctx,
		accountID,
		9999,
		false,
		false,
		id.Highest,
		id.Lowest,
		false,
		false,
	)
	if err != nil {
		suite.FailNow(err.Error())
	}

	// Remove each status.
	for _, status := range statuses {
		if err := suite.state.DB.DeleteStatusByID(ctx, status.ID); err != nil {
			suite.FailNow(err.Error())
		}
	}
}

func (suite *GetTestSuite) TestGetNewTimelinePageDown() {
	var (
		ctx         = context.Background()
		testAccount = suite.testAccounts["local_account_1"]
		maxID       = ""
		sinceID     = ""
		minID       = ""
		limit       = 5
		local       = false
	)

	// Get 5 from the top.
	statuses, err := suite.state.Timelines.Home.GetTimeline(
		ctx,
		testAccount.ID,
		maxID,
		sinceID,
		minID,
		limit,
		local,
	)
	if err != nil {
		suite.FailNow(err.Error())
	}
	suite.checkStatuses(statuses, id.Highest, id.Lowest, 5)

	// Get 5 from next maxID.
	maxID = statuses[len(statuses)-1].GetID()
	statuses, err = suite.state.Timelines.Home.GetTimeline(
		ctx,
		testAccount.ID,
		maxID,
		sinceID,
		minID,
		limit,
		local,
	)
	if err != nil {
		suite.FailNow(err.Error())
	}
	suite.checkStatuses(statuses, maxID, id.Lowest, 5)
}

func (suite *GetTestSuite) TestGetNewTimelinePageUp() {
	var (
		ctx         = context.Background()
		testAccount = suite.testAccounts["local_account_1"]
		maxID       = ""
		sinceID     = ""
		minID       = id.Lowest
		limit       = 5
		local       = false
	)

	// Get 5 from the back.
	statuses, err := suite.state.Timelines.Home.GetTimeline(
		ctx,
		testAccount.ID,
		maxID,
		sinceID,
		minID,
		limit,
		local,
	)
	if err != nil {
		suite.FailNow(err.Error())
	}
	suite.checkStatuses(statuses, id.Highest, minID, 5)

	// Page up from next minID.
	minID = statuses[0].GetID()
	statuses, err = suite.state.Timelines.Home.GetTimeline(
		ctx,
		testAccount.ID,
		maxID,
		sinceID,
		minID,
		limit,
		local,
	)
	if err != nil {
		suite.FailNow(err.Error())
	}
	suite.checkStatuses(statuses, id.Highest, minID, 5)
}

func (suite *GetTestSuite) TestGetNewTimelineMoreThanPossible() {
	var (
		ctx         = context.Background()
		testAccount = suite.testAccounts["local_account_1"]
		maxID       = ""
		sinceID     = ""
		minID       = ""
		limit       = 100
		local       = false
	)

	// Get 100 from the top.
	statuses, err := suite.state.Timelines.Home.GetTimeline(
		ctx,
		testAccount.ID,
		maxID,
		sinceID,
		minID,
		limit,
		local,
	)
	if err != nil {
		suite.FailNow(err.Error())
	}
	suite.checkStatuses(statuses, id.Highest, id.Lowest, 19)
}

func (suite *GetTestSuite) TestGetNewTimelineMoreThanPossiblePageUp() {
	var (
		ctx         = context.Background()
		testAccount = suite.testAccounts["local_account_1"]
		maxID       = ""
		sinceID     = ""
		minID       = id.Lowest
		limit       = 100
		local       = false
	)

	// Get 100 from the back.
	statuses, err := suite.state.Timelines.Home.GetTimeline(
		ctx,
		testAccount.ID,
		maxID,
		sinceID,
		minID,
		limit,
		local,
	)
	if err != nil {
		suite.FailNow(err.Error())
	}
	suite.checkStatuses(statuses, id.Highest, id.Lowest, 19)
}

func (suite *GetTestSuite) TestGetNewTimelineNoFollowing() {
	var (
		ctx         = context.Background()
		testAccount = suite.testAccounts["local_account_1"]
		maxID       = ""
		sinceID     = ""
		minID       = ""
		limit       = 10
		local       = false
	)

	suite.emptyAccountFollows(ctx, testAccount.ID)

	// Try to get 10 from the top of the timeline.
	statuses, err := suite.state.Timelines.Home.GetTimeline(
		ctx,
		testAccount.ID,
		maxID,
		sinceID,
		minID,
		limit,
		local,
	)
	if err != nil {
		suite.FailNow(err.Error())
	}
	suite.checkStatuses(statuses, id.Highest, id.Lowest, 7)

	for _, s := range statuses {
		if s.GetAccountID() != testAccount.ID {
			suite.FailNow("timeline with no follows should only contain posts by timeline owner account")
		}
	}
}

func (suite *GetTestSuite) TestGetNewTimelineNoFollowingNoStatuses() {
	var (
		ctx         = context.Background()
		testAccount = suite.testAccounts["local_account_1"]
		maxID       = ""
		sinceID     = ""
		minID       = ""
		limit       = 5
		local       = false
	)

	suite.emptyAccountFollows(ctx, testAccount.ID)
	suite.emptyAccountStatuses(ctx, testAccount.ID)

	// Try to get 5 from the top of the timeline.
	statuses, err := suite.state.Timelines.Home.GetTimeline(
		ctx,
		testAccount.ID,
		maxID,
		sinceID,
		minID,
		limit,
		local,
	)
	if err != nil {
		suite.FailNow(err.Error())
	}
	suite.checkStatuses(statuses, id.Highest, id.Lowest, 0)
}

func (suite *GetTestSuite) TestGetNoParams() {
	var (
		ctx         = context.Background()
		testAccount = suite.testAccounts["local_account_1"]
		maxID       = ""
		sinceID     = ""
		minID       = ""
		limit       = 10
		local       = false
	)

	suite.fillTimeline(testAccount.ID)

	// Get 10 statuses from the top (no params).
	statuses, err := suite.state.Timelines.Home.GetTimeline(
		ctx,
		testAccount.ID,
		maxID,
		sinceID,
		minID,
		limit,
		local,
	)
	if err != nil {
		suite.FailNow(err.Error())
	}

	suite.checkStatuses(statuses, id.Highest, id.Lowest, 10)

	// First status should have the highest ID in the testrig.
	suite.Equal(suite.highestStatusID, statuses[0].GetID())
}

func (suite *GetTestSuite) TestGetMaxID() {
	var (
		ctx         = context.Background()
		testAccount = suite.testAccounts["local_account_1"]
		maxID       = "01F8MHBQCBTDKN6X5VHGMMN4MA"
		sinceID     = ""
		minID       = ""
		limit       = 10
		local       = false
	)

	suite.fillTimeline(testAccount.ID)

	// Ask for 10 with a max ID somewhere in the middle of the stack.
	statuses, err := suite.state.Timelines.Home.GetTimeline(
		ctx,
		testAccount.ID,
		maxID,
		sinceID,
		minID,
		limit,
		local,
	)
	if err != nil {
		suite.FailNow(err.Error())
	}

	// We'll only get 6 statuses back.
	suite.checkStatuses(statuses, maxID, id.Lowest, 6)
}

func (suite *GetTestSuite) TestGetSinceID() {
	var (
		ctx         = context.Background()
		testAccount = suite.testAccounts["local_account_1"]
		maxID       = ""
		sinceID     = "01F8MHBQCBTDKN6X5VHGMMN4MA"
		minID       = ""
		limit       = 10
		local       = false
	)

	suite.fillTimeline(testAccount.ID)

	// Ask for 10 with a since ID somewhere in the middle of the stack.
	statuses, err := suite.state.Timelines.Home.GetTimeline(
		ctx,
		testAccount.ID,
		maxID,
		sinceID,
		minID,
		limit,
		local,
	)
	if err != nil {
		suite.FailNow(err.Error())
	}

	suite.checkStatuses(statuses, id.Highest, sinceID, 10)

	// The first status in the stack should have the highest ID of all
	// in the testrig, because we're paging down.
	suite.Equal(suite.highestStatusID, statuses[0].GetID())
}

func (suite *GetTestSuite) TestGetSinceIDOneOnly() {
	var (
		ctx         = context.Background()
		testAccount = suite.testAccounts["local_account_1"]
		maxID       = ""
		sinceID     = "01F8MHBQCBTDKN6X5VHGMMN4MA"
		minID       = ""
		limit       = 1
		local       = false
	)

	suite.fillTimeline(testAccount.ID)

	// Ask for 1 with a since ID somewhere in the middle of the stack.
	statuses, err := suite.state.Timelines.Home.GetTimeline(
		ctx,
		testAccount.ID,
		maxID,
		sinceID,
		minID,
		limit,
		local,
	)
	if err != nil {
		suite.FailNow(err.Error())
	}

	suite.checkStatuses(statuses, id.Highest, sinceID, 1)

	// The one status we got back should have the highest ID of all in
	// the testrig, because using sinceID means we're paging down.
	suite.Equal(suite.highestStatusID, statuses[0].GetID())
}

func (suite *GetTestSuite) TestGetMinID() {
	var (
		ctx         = context.Background()
		testAccount = suite.testAccounts["local_account_1"]
		maxID       = ""
		sinceID     = ""
		minID       = "01F8MHBQCBTDKN6X5VHGMMN4MA"
		limit       = 5
		local       = false
	)

	suite.fillTimeline(testAccount.ID)

	// Ask for 5 with a min ID somewhere in the middle of the stack.
	statuses, err := suite.state.Timelines.Home.GetTimeline(
		ctx,
		testAccount.ID,
		maxID,
		sinceID,
		minID,
		limit,
		local,
	)
	if err != nil {
		suite.FailNow(err.Error())
	}

	suite.checkStatuses(statuses, id.Highest, minID, 5)

	// We're paging up so even the highest status ID in the pile
	// shouldn't be the highest ID we have.
	suite.NotEqual(suite.highestStatusID, statuses[0])
}

func (suite *GetTestSuite) TestGetMinIDOneOnly() {
	var (
		ctx         = context.Background()
		testAccount = suite.testAccounts["local_account_1"]
		maxID       = ""
		sinceID     = ""
		minID       = "01F8MHBQCBTDKN6X5VHGMMN4MA"
		limit       = 1
		local       = false
	)

	suite.fillTimeline(testAccount.ID)

	// Ask for 1 with a min ID somewhere in the middle of the stack.
	statuses, err := suite.state.Timelines.Home.GetTimeline(
		ctx,
		testAccount.ID,
		maxID,
		sinceID,
		minID,
		limit,
		local,
	)
	if err != nil {
		suite.FailNow(err.Error())
	}

	suite.checkStatuses(statuses, id.Highest, minID, 1)

	// The one status we got back should have the an ID equal to the
	// one ID immediately newer than it.
	suite.Equal("01F8MHC0H0A7XHTVH5F596ZKBM", statuses[0].GetID())
}

func (suite *GetTestSuite) TestGetMinIDFromLowestInTestrig() {
	var (
		ctx         = context.Background()
		testAccount = suite.testAccounts["local_account_1"]
		maxID       = ""
		sinceID     = ""
		minID       = suite.lowestStatusID
		limit       = 1
		local       = false
	)

	suite.fillTimeline(testAccount.ID)

	// Ask for 1 with minID equal to the lowest status in the testrig.
	statuses, err := suite.state.Timelines.Home.GetTimeline(
		ctx,
		testAccount.ID,
		maxID,
		sinceID,
		minID,
		limit,
		local,
	)
	if err != nil {
		suite.FailNow(err.Error())
	}

	suite.checkStatuses(statuses, id.Highest, minID, 1)

	// The one status we got back should have an id higher than
	// the lowest status in the testrig, since minID is not inclusive.
	suite.Greater(statuses[0].GetID(), suite.lowestStatusID)
}

func (suite *GetTestSuite) TestGetMinIDFromLowestPossible() {
	var (
		ctx         = context.Background()
		testAccount = suite.testAccounts["local_account_1"]
		maxID       = ""
		sinceID     = ""
		minID       = id.Lowest
		limit       = 1
		local       = false
	)

	suite.fillTimeline(testAccount.ID)

	// Ask for 1 with the lowest possible min ID.
	statuses, err := suite.state.Timelines.Home.GetTimeline(
		ctx,
		testAccount.ID,
		maxID,
		sinceID,
		minID,
		limit,
		local,
	)
	if err != nil {
		suite.FailNow(err.Error())
	}

	suite.checkStatuses(statuses, id.Highest, minID, 1)

	// The one status we got back should have the an ID equal to the
	// lowest ID status in the test rig.
	suite.Equal(suite.lowestStatusID, statuses[0].GetID())
}

func (suite *GetTestSuite) TestGetBetweenID() {
	var (
		ctx         = context.Background()
		testAccount = suite.testAccounts["local_account_1"]
		maxID       = "01F8MHCP5P2NWYQ416SBA0XSEV"
		sinceID     = ""
		minID       = "01F8MHBQCBTDKN6X5VHGMMN4MA"
		limit       = 10
		local       = false
	)

	suite.fillTimeline(testAccount.ID)

	// Ask for 10 between these two IDs
	statuses, err := suite.state.Timelines.Home.GetTimeline(
		ctx,
		testAccount.ID,
		maxID,
		sinceID,
		minID,
		limit,
		local,
	)
	if err != nil {
		suite.FailNow(err.Error())
	}

	// There's only two statuses between these two IDs.
	suite.checkStatuses(statuses, maxID, minID, 2)
}

func (suite *GetTestSuite) TestGetBetweenIDImpossible() {
	var (
		ctx         = context.Background()
		testAccount = suite.testAccounts["local_account_1"]
		maxID       = id.Lowest
		sinceID     = ""
		minID       = id.Highest
		limit       = 10
		local       = false
	)

	suite.fillTimeline(testAccount.ID)

	// Ask for 10 between these two IDs which present
	// an impossible query.
	statuses, err := suite.state.Timelines.Home.GetTimeline(
		ctx,
		testAccount.ID,
		maxID,
		sinceID,
		minID,
		limit,
		local,
	)
	if err != nil {
		suite.FailNow(err.Error())
	}

	// We should have nothing back.
	suite.checkStatuses(statuses, maxID, minID, 0)
}

func (suite *GetTestSuite) TestGetTimelinesAsync() {
	var (
		ctx           = context.Background()
		accountToNuke = suite.testAccounts["local_account_1"]
		maxID         = ""
		sinceID       = ""
		minID         = ""
		limit         = 5
		local         = false
		multiplier    = 5
	)

	// Nuke one account's statuses and follows,
	// as though the account had just been created.
	suite.emptyAccountFollows(ctx, accountToNuke.ID)
	suite.emptyAccountStatuses(ctx, accountToNuke.ID)

	// Get 5 statuses from each timeline in
	// our testrig at the same time, five times.
	wg := new(sync.WaitGroup)
	wg.Add(len(suite.testAccounts) * multiplier)

	for i := 0; i < multiplier; i++ {
		go func() {
			for _, testAccount := range suite.testAccounts {
				if _, err := suite.state.Timelines.Home.GetTimeline(
					ctx,
					testAccount.ID,
					maxID,
					sinceID,
					minID,
					limit,
					local,
				); err != nil {
					suite.FailNow(err.Error())
				}

				wg.Done()
			}
		}()
	}

	wg.Wait() // Wait until all get calls have returned.
}

func TestGetTestSuite(t *testing.T) {
	suite.Run(t, new(GetTestSuite))
}