// 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 federatingdb_test

import (
	"encoding/json"
	"testing"
	"time"

	"github.com/stretchr/testify/suite"
	"github.com/superseriousbusiness/activity/streams"
	"github.com/superseriousbusiness/activity/streams/vocab"
	"github.com/superseriousbusiness/gotosocial/internal/ap"
	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
	"github.com/superseriousbusiness/gotosocial/internal/messages"
)

type MoveTestSuite struct {
	FederatingDBTestSuite
}

func (suite *MoveTestSuite) move(
	receivingAcct *gtsmodel.Account,
	requestingAcct *gtsmodel.Account,
	moveStr string,
) error {
	ctx := createTestContext(receivingAcct, requestingAcct)

	rawMove := make(map[string]interface{})
	if err := json.Unmarshal([]byte(moveStr), &rawMove); err != nil {
		suite.FailNow(err.Error())
	}

	t, err := streams.ToType(ctx, rawMove)
	if err != nil {
		suite.FailNow(err.Error())
	}

	move, ok := t.(vocab.ActivityStreamsMove)
	if !ok {
		suite.FailNow("", "couldn't cast %T to Move", t)
	}

	return suite.federatingDB.Move(ctx, move)
}

func (suite *MoveTestSuite) TestMove() {
	var (
		receivingAcct  = suite.testAccounts["local_account_1"]
		requestingAcct = suite.testAccounts["remote_account_1"]
		moveStr1       = `{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "http://fossbros-anonymous.io/users/foss_satan/moves/01HR9FDFCAGM7JYPMWNTFRDQE9",
  "actor": "http://fossbros-anonymous.io/users/foss_satan",
  "type": "Move",
  "object": "http://fossbros-anonymous.io/users/foss_satan",
  "target": "https://turnip.farm/users/turniplover6969",
  "to": "http://fossbros-anonymous.io/users/foss_satan/followers"
}`
	)

	// Trigger the move.
	suite.move(receivingAcct, requestingAcct, moveStr1)

	// Should be a message heading to the processor.
	var msg messages.FromFediAPI
	select {
	case msg = <-suite.fromFederator:
		// Fine.
	case <-time.After(5 * time.Second):
		suite.FailNow("", "timeout waiting for suite.fromFederator")
	}
	suite.Equal(ap.ObjectProfile, msg.APObjectType)
	suite.Equal(ap.ActivityMove, msg.APActivityType)

	// Stub Move should be on the message.
	move, ok := msg.GTSModel.(*gtsmodel.Move)
	if !ok {
		suite.FailNow("", "could not cast %T to *gtsmodel.Move", msg.GTSModel)
	}
	suite.Equal("http://fossbros-anonymous.io/users/foss_satan", move.OriginURI)
	suite.Equal("https://turnip.farm/users/turniplover6969", move.TargetURI)

	// Trigger the same move again.
	suite.move(receivingAcct, requestingAcct, moveStr1)

	// Should be a message heading to the processor
	// since this is just a straight up retry.
	select {
	case msg = <-suite.fromFederator:
		// Fine.
	case <-time.After(5 * time.Second):
		suite.FailNow("", "timeout waiting for suite.fromFederator")
	}
	suite.Equal(ap.ObjectProfile, msg.APObjectType)
	suite.Equal(ap.ActivityMove, msg.APActivityType)

	// Same as the first Move, but with a different ID.
	moveStr2 := `{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "http://fossbros-anonymous.io/users/foss_satan/moves/01HR9XWDD25CKXHW82MYD1GDAR",
  "actor": "http://fossbros-anonymous.io/users/foss_satan",
  "type": "Move",
  "object": "http://fossbros-anonymous.io/users/foss_satan",
  "target": "https://turnip.farm/users/turniplover6969",
  "to": "http://fossbros-anonymous.io/users/foss_satan/followers"
}`

	// Trigger the move.
	suite.move(receivingAcct, requestingAcct, moveStr2)

	// Should be a message heading to the processor
	// since this is just a retry with a different ID.
	select {
	case msg = <-suite.fromFederator:
		// Fine.
	case <-time.After(5 * time.Second):
		suite.FailNow("", "timeout waiting for suite.fromFederator")
	}
	suite.Equal(ap.ObjectProfile, msg.APObjectType)
	suite.Equal(ap.ActivityMove, msg.APActivityType)
}

func (suite *MoveTestSuite) TestBadMoves() {
	var (
		receivingAcct  = suite.testAccounts["local_account_1"]
		requestingAcct = suite.testAccounts["remote_account_1"]
	)

	type testStruct struct {
		moveStr string
		err     string
	}

	for _, t := range []testStruct{
		{
			// Move signed by someone else.
			moveStr: `{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "http://fossbros-anonymous.io/users/foss_satan/moves/01HR9FDFCAGM7JYPMWNTFRDQE9",
  "actor": "http://fossbros-anonymous.io/users/someone_else",
  "type": "Move",
  "object": "http://fossbros-anonymous.io/users/foss_satan",
  "target": "https://turnip.farm/users/turniplover6969",
  "to": "http://fossbros-anonymous.io/users/foss_satan/followers"
}`,
			err: "Move was signed by http://fossbros-anonymous.io/users/foss_satan but actor was http://fossbros-anonymous.io/users/someone_else",
		},
		{
			// Actor and object not the same.
			moveStr: `{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "http://fossbros-anonymous.io/users/foss_satan/moves/01HR9FDFCAGM7JYPMWNTFRDQE9",
  "actor": "http://fossbros-anonymous.io/users/foss_satan",
  "type": "Move",
  "object": "http://fossbros-anonymous.io/users/someone_else",
  "target": "https://turnip.farm/users/turniplover6969",
  "to": "http://fossbros-anonymous.io/users/foss_satan/followers"
}`,
			err: "Move was signed by http://fossbros-anonymous.io/users/foss_satan but object was http://fossbros-anonymous.io/users/someone_else",
		},
		{
			// Object and target the same.
			moveStr: `{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "http://fossbros-anonymous.io/users/foss_satan/moves/01HR9FDFCAGM7JYPMWNTFRDQE9",
  "actor": "http://fossbros-anonymous.io/users/foss_satan",
  "type": "Move",
  "object": "http://fossbros-anonymous.io/users/foss_satan",
  "target": "http://fossbros-anonymous.io/users/foss_satan",
  "to": "http://fossbros-anonymous.io/users/foss_satan/followers"
}`,
			err: "Move target and origin were the same (http://fossbros-anonymous.io/users/foss_satan)",
		},
	} {
		// Trigger the move.
		err := suite.move(receivingAcct, requestingAcct, t.moveStr)
		if t.err != "" {
			suite.EqualError(err, t.err)
		}
	}
}

func TestMoveTestSuite(t *testing.T) {
	suite.Run(t, &MoveTestSuite{})
}