//go:build system_test && linux

package system

import (
	"archive/tar"
	"compress/gzip"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tidwall/gjson"
)

func TestChainUpgrade(t *testing.T) {
	// Scenario:
	// start a legacy chain with some state
	// when a chain upgrade proposal is executed
	// then the chain upgrades successfully

	legacyBinary := FetchExecutable(t, "v0.41.0")
	t.Logf("+++ legacy binary: %s\n", legacyBinary)
	currentBranchBinary := sut.ExecBinary
	sut.ExecBinary = legacyBinary
	sut.SetupChain()
	votingPeriod := 5 * time.Second // enough time to vote
	sut.ModifyGenesisJSON(t, SetGovVotingPeriod(t, votingPeriod))

	const (
		upgradeHeight int64 = 22
		upgradeName         = "v0.50"
	)

	sut.StartChain(t, fmt.Sprintf("--halt-height=%d", upgradeHeight))

	cli := NewWasmdCLI(t, sut, verbose)

	// set some state to ensure that migrations work
	verifierAddr := cli.AddKey("verifier")
	beneficiary := randomBech32Addr()
	cli.FundAddress(verifierAddr, "1000stake")

	t.Log("Launch hackatom contract")
	codeID := cli.WasmStore("./testdata/hackatom.wasm.gzip")
	initMsg := fmt.Sprintf(`{"verifier":%q, "beneficiary":%q}`, verifierAddr, beneficiary)
	contractAddr := cli.WasmInstantiate(codeID, initMsg, "--admin="+defaultSrcAddr, "--label=label1", "--from="+defaultSrcAddr,
		"--amount=1000000stake")

	gotRsp := cli.QuerySmart(contractAddr, `{"verifier":{}}`)
	require.Equal(t, fmt.Sprintf(`{"data":{"verifier":"%s"}}`, verifierAddr), gotRsp)

	// submit upgrade proposal
	proposal := fmt.Sprintf(`
{
 "messages": [
  {
   "@type": "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade",
   "authority": "wasm10d07y265gmmuvt4z0w9aw880jnsr700js7zslc",
   "plan": {
    "name": %q,
    "height": "%d"
   }
  }
 ],
 "metadata": "ipfs://CID",
 "deposit": "100000000stake",
 "title": "my upgrade",
 "summary": "testing"
}`, upgradeName, upgradeHeight)
	proposalID := cli.SubmitAndVoteGovProposal(proposal)
	t.Logf("current_height: %d\n", sut.currentHeight)
	raw := cli.CustomQuery("q", "gov", "proposal", proposalID)
	t.Log(raw)
	sut.AwaitBlockHeight(t, upgradeHeight-1)
	t.Logf("current_height: %d\n", sut.currentHeight)
	raw = cli.CustomQuery("q", "gov", "proposal", proposalID)
	proposalStatus := gjson.Get(raw, "status").String()
	require.Equal(t, "PROPOSAL_STATUS_PASSED", proposalStatus, raw)

	t.Log("waiting for upgrade info")
	sut.AwaitUpgradeInfo(t)
	sut.StopChain()

	t.Log("Upgrade height was reached. Upgrading chain")
	sut.ExecBinary = currentBranchBinary
	sut.StartChain(t)
	cli = NewWasmdCLI(t, sut, verbose)

	// ensure that state matches expectations
	gotRsp = cli.QuerySmart(contractAddr, `{"verifier":{}}`)
	require.Equal(t, fmt.Sprintf(`{"data":{"verifier":"%s"}}`, verifierAddr), gotRsp)
	// and contract execution works as expected
	RequireTxSuccess(t, cli.WasmExecute(contractAddr, `{"release":{}}`, verifierAddr))
	assert.Equal(t, int64(1_000_000), cli.QueryBalance(beneficiary, "stake"))
}

const cacheDir = "binaries"

// FetchExecutable to download and extract tar.gz for linux
func FetchExecutable(t *testing.T, version string) string {
	// use local cache
	cacheFolder := filepath.Join(WorkDir, cacheDir)
	err := os.MkdirAll(cacheFolder, 0o777)
	if err != nil && !os.IsExist(err) {
		panic(err)
	}

	cacheFile := filepath.Join(cacheFolder, fmt.Sprintf("%s_%s", execBinaryName, version))
	if _, err := os.Stat(cacheFile); err == nil {
		return cacheFile
	}
	t.Logf("+++ version not in cache, downloading from github")

	// then download from GH releases: only works with Linux currently as we are not publishing OSX binaries
	const releaseUrl = "https://wasmd.mleku.dev/releases/download/%s/wasmd-%s-linux-amd64.tar.gz"
	destDir := t.TempDir()
	rsp, err := http.Get(fmt.Sprintf(releaseUrl, version, version))
	require.NoError(t, err)
	defer rsp.Body.Close()
	gzr, err := gzip.NewReader(rsp.Body)
	require.NoError(t, err)
	defer gzr.Close()
	tr := tar.NewReader(gzr)

	var workFileName string
	for {
		header, err := tr.Next()
		switch {
		case err == io.EOF:
			require.NotEmpty(t, workFileName)
			require.NoError(t, os.Rename(workFileName, cacheFile))
			return cacheFile
		case err != nil:
			require.NoError(t, err)
		case header == nil:
			continue
		}
		workFileName = filepath.Join(destDir, header.Name)
		switch header.Typeflag {
		case tar.TypeDir:
			t.Fatalf("unexpected type")
		case tar.TypeReg:
			f, err := os.OpenFile(workFileName, os.O_CREATE|os.O_RDWR, os.FileMode(0o755))
			require.NoError(t, err)
			_, err = io.Copy(f, tr)
			require.NoError(t, err)
			_ = f.Close()
		}
	}
}
