package keeper_test

import (
	"fmt"
	"math/rand"

	"github.com/cosmos/cosmos-sdk/contrib/x/nft"
)

func (s *TestSuite) TestBatchMint() {
	receiver := s.addrs[0]
	testCases := []struct {
		msg      string
		malleate func([]nft.NFT)
		tokens   []nft.NFT
		expPass  bool
	}{
		{
			"success with empty nft",
			func(tokens []nft.NFT) {
				s.saveClass(tokens)
			},
			[]nft.NFT{},
			true,
		},
		{
			"success with single nft",
			func(tokens []nft.NFT) {
				s.saveClass(tokens)
			},
			[]nft.NFT{
				{ClassId: "classID1", Id: "nftID1"},
			},
			true,
		},
		{
			"success with multiple nft",
			func(tokens []nft.NFT) {
				s.saveClass(tokens)
			},
			[]nft.NFT{
				{ClassId: "classID1", Id: "nftID1"},
				{ClassId: "classID1", Id: "nftID2"},
			},
			true,
		},
		{
			"success with multiple class and multiple nft",
			func(tokens []nft.NFT) {
				s.saveClass(tokens)
			},
			[]nft.NFT{
				{ClassId: "classID1", Id: "nftID1"},
				{ClassId: "classID1", Id: "nftID2"},
				{ClassId: "classID2", Id: "nftID1"},
				{ClassId: "classID2", Id: "nftID2"},
			},
			true,
		},
		{
			"failed with repeated nft",
			func(tokens []nft.NFT) {
				s.saveClass(tokens)
			},
			[]nft.NFT{
				{ClassId: "classID1", Id: "nftID1"},
				{ClassId: "classID1", Id: "nftID1"},
				{ClassId: "classID2", Id: "nftID2"},
			},
			false,
		},
		{
			"failed with not exist class",
			func(tokens []nft.NFT) {
				// do nothing
			},
			[]nft.NFT{
				{ClassId: "classID1", Id: "nftID1"},
				{ClassId: "classID1", Id: "nftID1"},
				{ClassId: "classID2", Id: "nftID2"},
			},
			false,
		},
		{
			"failed with exist nft",
			func(tokens []nft.NFT) {
				s.saveClass(tokens)
				idx := rand.Intn(len(tokens))
				s.Require().NoError(s.nftKeeper.Mint(s.ctx, tokens[idx], receiver))
			},
			[]nft.NFT{
				{ClassId: "classID1", Id: "nftID1"},
				{ClassId: "classID1", Id: "nftID2"},
				{ClassId: "classID2", Id: "nftID2"},
			},
			false,
		},
	}

	for _, tc := range testCases {
		s.Run(fmt.Sprintf("Case %s", tc.msg), func() {
			s.SetupTest() // reset
			tc.malleate(tc.tokens)

			err := s.nftKeeper.BatchMint(s.ctx, tc.tokens, receiver)
			if tc.expPass {
				s.Require().NoError(err)

				classMap := groupByClassID(tc.tokens)
				for classID, tokens := range classMap {
					for _, token := range tokens {
						actNFT, has := s.nftKeeper.GetNFT(s.ctx, token.ClassId, token.Id)
						s.Require().True(has)
						s.Require().EqualValues(token, actNFT)

						owner := s.nftKeeper.GetOwner(s.ctx, token.ClassId, token.Id)
						s.Require().True(receiver.Equals(owner))
					}

					actNFTs := s.nftKeeper.GetNFTsOfClass(s.ctx, classID)
					s.Require().EqualValues(tokens, actNFTs)

					actNFTs = s.nftKeeper.GetNFTsOfClassByOwner(s.ctx, classID, receiver)
					s.Require().EqualValues(tokens, actNFTs)

					balance := s.nftKeeper.GetBalance(s.ctx, classID, receiver)
					s.Require().EqualValues(len(tokens), balance)

					supply := s.nftKeeper.GetTotalSupply(s.ctx, classID)
					s.Require().EqualValues(len(tokens), supply)
				}
				return
			}
			s.Require().Error(err)
		})
	}
}

func (s *TestSuite) TestBatchBurn() {
	receiver := s.addrs[0]
	tokens := []nft.NFT{
		{ClassId: "classID1", Id: "nftID1"},
		{ClassId: "classID1", Id: "nftID2"},
		{ClassId: "classID2", Id: "nftID1"},
		{ClassId: "classID2", Id: "nftID2"},
	}

	testCases := []struct {
		msg      string
		malleate func()
		classID  string
		nftIDs   []string
		expPass  bool
	}{
		{
			"success",
			func() {
				s.saveClass(tokens)
				s.Require().NoError(s.nftKeeper.BatchMint(s.ctx, tokens, receiver))
			},
			"classID1",
			[]string{"nftID1", "nftID2"},
			true,
		},
		{
			"failed with not exist classID",
			func() {},
			"classID1",
			[]string{"nftID1", "nftID2"},
			false,
		},
		{
			"failed with not exist nftID",
			func() {
				s.saveClass(tokens)
			},
			"classID1",
			[]string{"nftID1", "nftID2"},
			false,
		},
	}

	for _, tc := range testCases {
		s.Run(fmt.Sprintf("Case %s", tc.msg), func() {
			s.SetupTest() // reset
			tc.malleate()

			err := s.nftKeeper.BatchBurn(s.ctx, tc.classID, tc.nftIDs)
			if tc.expPass {
				s.Require().NoError(err)
				for _, nftID := range tc.nftIDs {
					s.Require().False(s.nftKeeper.HasNFT(s.ctx, tc.classID, nftID))
				}
				return
			}
			s.Require().Error(err)
		})
	}
}

func (s *TestSuite) TestBatchUpdate() {
	receiver := s.addrs[0]
	tokens := []nft.NFT{
		{ClassId: "classID1", Id: "nftID1"},
		{ClassId: "classID1", Id: "nftID2"},
		{ClassId: "classID2", Id: "nftID1"},
		{ClassId: "classID2", Id: "nftID2"},
	}
	testCases := []struct {
		msg      string
		malleate func()
		tokens   []nft.NFT
		expPass  bool
	}{
		{
			"success",
			func() {
				s.saveClass(tokens)
				s.Require().NoError(s.nftKeeper.BatchMint(s.ctx, tokens, receiver))
			},
			[]nft.NFT{
				{ClassId: "classID1", Id: "nftID1", Uri: "nftID1_URI"},
				{ClassId: "classID2", Id: "nftID2", Uri: "nftID2_URI"},
			},
			true,
		},
		{
			"failed with not exist classID",
			func() {},
			[]nft.NFT{
				{ClassId: "classID1", Id: "nftID1", Uri: "nftID1_URI"},
				{ClassId: "classID2", Id: "nftID2", Uri: "nftID2_URI"},
			},
			false,
		},
		{
			"failed with not exist nftID",
			func() {
				s.saveClass(tokens)
			},
			[]nft.NFT{
				{ClassId: "classID1", Id: "nftID1", Uri: "nftID1_URI"},
				{ClassId: "classID2", Id: "nftID2", Uri: "nftID2_URI"},
			},
			false,
		},
	}

	for _, tc := range testCases {
		s.Run(fmt.Sprintf("Case %s", tc.msg), func() {
			s.SetupTest() // reset
			tc.malleate()

			err := s.nftKeeper.BatchUpdate(s.ctx, tc.tokens)
			if tc.expPass {
				s.Require().NoError(err)
				for _, token := range tc.tokens {
					actToken, found := s.nftKeeper.GetNFT(s.ctx, token.ClassId, token.Id)
					s.Require().True(found)
					s.Require().EqualValues(token, actToken)
				}
				return
			}
			s.Require().Error(err)
		})
	}
}

func (s *TestSuite) TestBatchTransfer() {
	owner := s.addrs[0]
	receiver := s.addrs[1]
	tokens := []nft.NFT{
		{ClassId: "classID1", Id: "nftID1"},
		{ClassId: "classID1", Id: "nftID2"},
		{ClassId: "classID2", Id: "nftID1"},
		{ClassId: "classID2", Id: "nftID2"},
	}
	testCases := []struct {
		msg      string
		malleate func()
		classID  string
		nftIDs   []string
		expPass  bool
	}{
		{
			"success",
			func() {
				s.saveClass(tokens)
				s.Require().NoError(s.nftKeeper.BatchMint(s.ctx, tokens, owner))
			},
			"classID1",
			[]string{"nftID1", "nftID2"},
			true,
		},
		{
			"failed with not exist classID",
			func() {
				s.saveClass(tokens)
				s.Require().NoError(s.nftKeeper.BatchMint(s.ctx, tokens, receiver))
			},
			"classID3",
			[]string{"nftID1", "nftID2"},
			false,
		},
		{
			"failed with not exist nftID",
			func() {
				s.saveClass(tokens)
			},
			"classID1",
			[]string{"nftID1", "nftID2"},
			false,
		},
	}

	for _, tc := range testCases {
		s.Run(fmt.Sprintf("Case %s", tc.msg), func() {
			s.SetupTest() // reset
			tc.malleate()

			err := s.nftKeeper.BatchTransfer(s.ctx, tc.classID, tc.nftIDs, receiver)
			if tc.expPass {
				s.Require().NoError(err)
				for _, nftID := range tc.nftIDs {
					actOwner := s.nftKeeper.GetOwner(s.ctx, tc.classID, nftID)
					s.Require().EqualValues(receiver, actOwner)
				}
				return
			}
			s.Require().Error(err)
		})
	}
}

func groupByClassID(tokens []nft.NFT) map[string][]nft.NFT {
	classMap := make(map[string][]nft.NFT, len(tokens))
	for _, token := range tokens {
		if _, ok := classMap[token.ClassId]; !ok {
			classMap[token.ClassId] = make([]nft.NFT, 0)
		}
		classMap[token.ClassId] = append(classMap[token.ClassId], token)
	}
	return classMap
}

func (s *TestSuite) saveClass(tokens []nft.NFT) {
	classMap := groupByClassID(tokens)
	for classID := range classMap {
		err := s.nftKeeper.SaveClass(s.ctx, nft.Class{Id: classID})
		s.Require().NoError(err)
	}
}
