// stm: #integration
package itests

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

	blocks "github.com/ipfs/go-block-format"
	"github.com/ipfs/go-cid"
	"github.com/ipld/go-car"
	"github.com/stretchr/testify/require"
	"golang.org/x/xerrors"

	"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/itests/kit"
)

// 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")
	selectedCid, _          = cid.Parse("bafkqaetgnfwc6mjpon2g64tbm5sxa33xmvza")
	carPieceSize            = abi.PaddedPieceSize(2097152)
	textSelector            = api.Selector("8/1/8/1/0/1/0")
	textSelectorNonLink     = api.Selector("8/1/8/1/0/1")
	textSelectorNonexistent = api.Selector("42")
	expectedResult          = "fil/1/storagepower"
)

func TestPartialRetrieval(t *testing.T) {
	//stm: @CHAIN_SYNCER_LOAD_GENESIS_001, @CHAIN_SYNCER_FETCH_TIPSET_001,
	//stm: @CHAIN_SYNCER_START_001, @CHAIN_SYNCER_SYNC_001, @BLOCKCHAIN_BEACON_VALIDATE_BLOCK_VALUES_01
	//stm: @CHAIN_SYNCER_COLLECT_CHAIN_001, @CHAIN_SYNCER_COLLECT_HEADERS_001, @CHAIN_SYNCER_VALIDATE_TIPSET_001
	//stm: @CHAIN_SYNCER_NEW_PEER_HEAD_001, @CHAIN_SYNCER_VALIDATE_MESSAGE_META_001, @CHAIN_SYNCER_STOP_001

	//stm: @CHAIN_INCOMING_HANDLE_INCOMING_BLOCKS_001, @CHAIN_INCOMING_VALIDATE_BLOCK_PUBSUB_001, @CHAIN_INCOMING_VALIDATE_MESSAGE_PUBSUB_001
	//stm: @CLIENT_RETRIEVAL_RETRIEVE_001
	ctx := context.Background()

	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 _, exportMerkleProof := range []bool{false, true} {
		for _, fullCycle := range []bool{false, true} {

			var retOrder api.RetrievalOrder
			var eref api.ExportRef

			if !fullCycle {
				eref.FromLocalCAR = sourceCar
			} 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.DataSelector = &textSelector
			eref.DAGs = append(eref.DAGs, api.DagSpec{
				DataSelector:      &textSelector,
				ExportMerkleProof: exportMerkleProof,
			})
			eref.Root = carRoot

			// test retrieval of either data or constructing a partial selective-car
			for _, retrieveAsCar := range []bool{false, true} {
				outFile := t.TempDir() + string(os.PathSeparator) + "ret-file" + retOrder.Root.String()

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

				// 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{
				Root:         carRoot,
				DataSelector: &textSelectorNonexistent,
			},
			api.ExportRef{
				Root:         carRoot,
				FromLocalCAR: sourceCar,
				DAGs:         []api.DagSpec{{DataSelector: &textSelectorNonexistent}},
			},
			&api.FileRef{},
		),
		fmt.Sprintf("parsing dag spec: path selection does not match a node within %s", carRoot),
	)

	// ensure non-boundary retrievals fail
	require.EqualError(
		t,
		testGenesisRetrieval(
			ctx,
			client,
			api.RetrievalOrder{
				Root:         carRoot,
				DataSelector: &textSelectorNonLink,
			},
			api.ExportRef{
				Root:         carRoot,
				FromLocalCAR: sourceCar,
				DAGs:         []api.DagSpec{{DataSelector: &textSelectorNonLink}},
			},
			&api.FileRef{},
		),
		fmt.Sprintf("parsing dag spec: 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, eref api.ExportRef, retRef *api.FileRef) error {

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

	if eref.FromLocalCAR == "" {
		rr, err := client.ClientRetrieve(ctx, retOrder)
		if err != nil {
			return err
		}
		eref.DealID = rr.DealID

		if err := client.ClientRetrieveWait(ctx, rr.DealID); err != nil {
			return xerrors.Errorf("retrieval wait: %w", err)
		}
	}

	err := client.ClientExport(ctx, eref, *retRef)
	if err != nil {
		return err
	}

	outFile, err := os.Open(retRef.Path)
	if err != nil {
		return err
	}

	defer outFile.Close() //nolint:errcheck

	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 eref.DAGs[0].ExportMerkleProof && cr.Header.Roots[0].String() != carRoot.String() {
			return fmt.Errorf("expected root cid '%s', got '%s'", carRoot.String(), cr.Header.Roots[0].String())
		} else if !eref.DAGs[0].ExportMerkleProof && cr.Header.Roots[0].String() != selectedCid.String() {
			return fmt.Errorf("expected root cid '%s', got '%s'", selectedCid.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 (eref.DAGs[0].ExportMerkleProof && len(blks) != 3) || (!eref.DAGs[0].ExportMerkleProof && len(blks) != 1) {
			return fmt.Errorf("expected a car file with 3/1 blocks, got one with %d instead", len(blks))
		}

		data = blks[len(blks)-1].RawData()
	}

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

	return nil
}