package itests

import (
	"context"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"testing"
	"time"

	"github.com/filecoin-project/go-fil-markets/storagemarket"
	"github.com/filecoin-project/go-state-types/abi"
	"github.com/filecoin-project/go-state-types/big"
	"github.com/filecoin-project/lotus/api"
	"github.com/filecoin-project/lotus/chain/actors/policy"
	"github.com/filecoin-project/lotus/itests/kit"
	blocks "github.com/ipfs/go-block-format"
	"github.com/ipfs/go-cid"
	"github.com/ipld/go-car"
	textselector "github.com/ipld/go-ipld-selector-text-lite"
	"github.com/stretchr/testify/require"
)

// use the mainnet carfile as text fixture: it will always be here
// https://dweb.link/ipfs/bafy2bzacecnamqgqmifpluoeldx7zzglxcljo6oja4vrmtj7432rphldpdmm2/8/1/8/1/0/1/0
var (
	sourceCar               = "../build/genesis/mainnet.car"
	carRoot, _              = cid.Parse("bafy2bzacecnamqgqmifpluoeldx7zzglxcljo6oja4vrmtj7432rphldpdmm2")
	carCommp, _             = cid.Parse("baga6ea4seaqmrivgzei3fmx5qxtppwankmtou6zvigyjaveu3z2zzwhysgzuina")
	carPieceSize            = abi.PaddedPieceSize(2097152)
	textSelector            = textselector.Expression("8/1/8/1/0/1/0")
	textSelectorNonLink     = textselector.Expression("8/1/8/1/0/1")
	textSelectorNonexistent = textselector.Expression("42")
	expectedResult          = "fil/1/storagepower"
)

func TestPartialRetrieval(t *testing.T) {

	ctx := context.Background()

	policy.SetPreCommitChallengeDelay(2)
	kit.EnableLargeSectors(t)
	kit.QuietMiningLogs()
	client, miner, ens := kit.EnsembleMinimal(t, kit.ThroughRPC(), kit.MockProofs(), kit.SectorSize(512<<20))
	dh := kit.NewDealHarness(t, client, miner, miner)
	ens.InterconnectAll().BeginMining(50 * time.Millisecond)

	_, err := client.ClientImport(ctx, api.FileRef{Path: sourceCar, IsCAR: true})
	require.NoError(t, err)

	caddr, err := client.WalletDefaultAddress(ctx)
	require.NoError(t, err)

	// first test retrieval from local car, then do an actual deal
	for _, fullCycle := range []bool{false, true} {

		var retOrder api.RetrievalOrder

		if !fullCycle {

			retOrder.FromLocalCAR = sourceCar
			retOrder.Root = carRoot

		} else {

			dp := dh.DefaultStartDealParams()
			dp.Data = &storagemarket.DataRef{
				// FIXME: figure out how to do this with an online partial transfer
				TransferType: storagemarket.TTManual,
				Root:         carRoot,
				PieceCid:     &carCommp,
				PieceSize:    carPieceSize.Unpadded(),
			}
			proposalCid := dh.StartDeal(ctx, dp)

			// Wait for the deal to reach StorageDealCheckForAcceptance on the client
			cd, err := client.ClientGetDealInfo(ctx, *proposalCid)
			require.NoError(t, err)
			require.Eventually(t, func() bool {
				cd, _ := client.ClientGetDealInfo(ctx, *proposalCid)
				return cd.State == storagemarket.StorageDealCheckForAcceptance
			}, 30*time.Second, 1*time.Second, "actual deal status is %s", storagemarket.DealStates[cd.State])

			err = miner.DealsImportData(ctx, *proposalCid, sourceCar)
			require.NoError(t, err)

			// Wait for the deal to be published, we should be able to start retrieval right away
			dh.WaitDealPublished(ctx, proposalCid)

			offers, err := client.ClientFindData(ctx, carRoot, nil)
			require.NoError(t, err)
			require.NotEmpty(t, offers, "no offers")

			retOrder = offers[0].Order(caddr)
		}

		retOrder.DatamodelPathSelector = &textSelector

		// test retrieval of either data or constructing a partial selective-car
		for _, retrieveAsCar := range []bool{false, true} {
			outFile, err := ioutil.TempFile(t.TempDir(), "ret-file")
			require.NoError(t, err)
			defer outFile.Close() //nolint:errcheck

			require.NoError(t, testGenesisRetrieval(
				ctx,
				client,
				retOrder,
				&api.FileRef{
					Path:  outFile.Name(),
					IsCAR: retrieveAsCar,
				},
				outFile,
			))

			// UGH if I do not sleep here, I get things like:
			/*
				retrieval failed: Retrieve failed: there is an active retrieval deal with peer 12D3KooWK9fB9a3HZ4PQLVmEQ6pweMMn5CAyKtumB71CPTnuBDi6 for payload CID bafy2bzacecnamqgqmifpluoeldx7zzglxcljo6oja4vrmtj7432rphldpdmm2 (retrieval deal ID 1631259332180384709, state DealStatusFinalizingBlockstore) - existing deal must be cancelled before starting a new retrieval deal:
					github.com/filecoin-project/lotus/node/impl/client.(*API).ClientRetrieve
						/home/circleci/project/node/impl/client/client.go:774
			*/
			time.Sleep(time.Second)
		}
	}

	// ensure non-existent paths fail
	require.EqualError(
		t,
		testGenesisRetrieval(
			ctx,
			client,
			api.RetrievalOrder{
				FromLocalCAR:          sourceCar,
				Root:                  carRoot,
				DatamodelPathSelector: &textSelectorNonexistent,
			},
			&api.FileRef{},
			nil,
		),
		fmt.Sprintf("retrieval failed: path selection '%s' does not match a node within %s", textSelectorNonexistent, carRoot),
	)

	// ensure non-boundary retrievals fail
	require.EqualError(
		t,
		testGenesisRetrieval(
			ctx,
			client,
			api.RetrievalOrder{
				FromLocalCAR:          sourceCar,
				Root:                  carRoot,
				DatamodelPathSelector: &textSelectorNonLink,
			},
			&api.FileRef{},
			nil,
		),
		fmt.Sprintf("retrieval failed: error while locating partial retrieval sub-root: unsupported selection path '%s' does not correspond to a block boundary (a.k.a. CID link)", textSelectorNonLink),
	)
}

func testGenesisRetrieval(ctx context.Context, client *kit.TestFullNode, retOrder api.RetrievalOrder, retRef *api.FileRef, outFile *os.File) error {

	if retOrder.Total.Nil() {
		retOrder.Total = big.Zero()
	}
	if retOrder.UnsealPrice.Nil() {
		retOrder.UnsealPrice = big.Zero()
	}

	err := client.ClientRetrieve(ctx, retOrder, retRef)
	if err != nil {
		return err
	}

	var data []byte
	if !retRef.IsCAR {

		data, err = io.ReadAll(outFile)
		if err != nil {
			return err
		}

	} else {

		cr, err := car.NewCarReader(outFile)
		if err != nil {
			return err
		}

		if len(cr.Header.Roots) != 1 {
			return fmt.Errorf("expected a single root in result car, got %d", len(cr.Header.Roots))
		} else if cr.Header.Roots[0].String() != carRoot.String() {
			return fmt.Errorf("expected root cid '%s', got '%s'", carRoot.String(), cr.Header.Roots[0].String())
		}

		blks := make([]blocks.Block, 0)
		for {
			b, err := cr.Next()
			if err == io.EOF {
				break
			} else if err != nil {
				return err
			}

			blks = append(blks, b)
		}

		if len(blks) != 3 {
			return fmt.Errorf("expected a car file with 3 blocks, got one with %d instead", len(blks))
		}

		data = blks[2].RawData()
	}

	if string(data) != expectedResult {
		return fmt.Errorf("retrieved data mismatch: expected '%s' got '%s'", expectedResult, data)
	}

	return nil
}
