package types

import (
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tendermint/tendermint/libs/log"
	tmproto "github.com/tendermint/tendermint/proto/tendermint/types"

	clienttypes "github.com/cosmos/cosmos-sdk/x/ibc/core/02-client/types"
	ibctmtypes "github.com/cosmos/cosmos-sdk/x/ibc/light-clients/07-tendermint/types"

	sdk "github.com/cosmos/cosmos-sdk/types"
)

func mustParseTime(s string) time.Time {
	t, err := time.Parse(time.RFC3339, s)
	if err != nil {
		panic(err)
	}
	return t
}

func TestPlanString(t *testing.T) {
	cs, err := clienttypes.PackClientState(&ibctmtypes.ClientState{})
	require.NoError(t, err)

	cases := map[string]struct {
		p      Plan
		expect string
	}{
		"with time": {
			p: Plan{
				Name: "due_time",
				Info: "https://foo.bar",
				Time: mustParseTime("2019-07-08T11:33:55Z"),
			},
			expect: "Upgrade Plan\n  Name: due_time\n  Time: 2019-07-08T11:33:55Z\n  Info: https://foo.bar.\n  Upgraded IBC Client: no upgraded client provided",
		},
		"with height": {
			p: Plan{
				Name:   "by height",
				Info:   "https://foo.bar/baz",
				Height: 7890,
			},
			expect: "Upgrade Plan\n  Name: by height\n  Height: 7890\n  Info: https://foo.bar/baz.\n  Upgraded IBC Client: no upgraded client provided",
		},
		"with IBC client": {
			p: Plan{
				Name:                "by height",
				Info:                "https://foo.bar/baz",
				Height:              7890,
				UpgradedClientState: cs,
			},
			expect: fmt.Sprintf("Upgrade Plan\n  Name: by height\n  Height: 7890\n  Info: https://foo.bar/baz.\n  Upgraded IBC Client: %s", &ibctmtypes.ClientState{}),
		},

		"neither": {
			p: Plan{
				Name: "almost-empty",
			},
			expect: "Upgrade Plan\n  Name: almost-empty\n  Height: 0\n  Info: .\n  Upgraded IBC Client: no upgraded client provided",
		},
	}

	for name, tc := range cases {
		tc := tc // copy to local variable for scopelint
		t.Run(name, func(t *testing.T) {
			s := tc.p.String()
			require.Equal(t, tc.expect, s)
		})
	}
}

func TestPlanValid(t *testing.T) {
	cs, err := clienttypes.PackClientState(&ibctmtypes.ClientState{})
	require.NoError(t, err)

	cases := map[string]struct {
		p     Plan
		valid bool
	}{
		"proper": {
			p: Plan{
				Name: "all-good",
				Info: "some text here",
				Time: mustParseTime("2019-07-08T11:33:55Z"),
			},
			valid: true,
		},
		"proper ibc upgrade": {
			p: Plan{
				Name:                "ibc-all-good",
				Info:                "some text here",
				Height:              123450000,
				UpgradedClientState: cs,
			},
			valid: true,
		},
		"proper by height": {
			p: Plan{
				Name:   "all-good",
				Height: 123450000,
			},
			valid: true,
		},
		"no name": {
			p: Plan{
				Height: 123450000,
			},
		},
		"no due at": {
			p: Plan{
				Name: "missing",
				Info: "important",
			},
		},
		"negative height": {
			p: Plan{
				Name:   "minus",
				Height: -12345,
			},
		},
		"time due date defined for IBC plan": {
			p: Plan{
				Name:                "ibc-all-good",
				Info:                "some text here",
				Time:                mustParseTime("2019-07-08T11:33:55Z"),
				UpgradedClientState: cs,
			},
			valid: false,
		},
	}

	for name, tc := range cases {
		tc := tc // copy to local variable for scopelint
		t.Run(name, func(t *testing.T) {
			err := tc.p.ValidateBasic()
			if tc.valid {
				assert.NoError(t, err)
			} else {
				assert.Error(t, err)
			}
		})
	}

}

func TestShouldExecute(t *testing.T) {
	cases := map[string]struct {
		p         Plan
		ctxTime   time.Time
		ctxHeight int64
		expected  bool
	}{
		"past time": {
			p: Plan{
				Name: "do-good",
				Info: "some text here",
				Time: mustParseTime("2019-07-08T11:33:55Z"),
			},
			ctxTime:   mustParseTime("2019-07-08T11:32:00Z"),
			ctxHeight: 100000,
			expected:  false,
		},
		"on time": {
			p: Plan{
				Name: "do-good",
				Time: mustParseTime("2019-07-08T11:33:55Z"),
			},
			ctxTime:   mustParseTime("2019-07-08T11:33:55Z"),
			ctxHeight: 100000,
			expected:  true,
		},
		"future time": {
			p: Plan{
				Name: "do-good",
				Time: mustParseTime("2019-07-08T11:33:55Z"),
			},
			ctxTime:   mustParseTime("2019-07-08T11:33:57Z"),
			ctxHeight: 100000,
			expected:  true,
		},
		"past height": {
			p: Plan{
				Name:   "do-good",
				Height: 1234,
			},
			ctxTime:   mustParseTime("2019-07-08T11:32:00Z"),
			ctxHeight: 1000,
			expected:  false,
		},
		"on height": {
			p: Plan{
				Name:   "do-good",
				Height: 1234,
			},
			ctxTime:   mustParseTime("2019-07-08T11:32:00Z"),
			ctxHeight: 1234,
			expected:  true,
		},
		"future height": {
			p: Plan{
				Name:   "do-good",
				Height: 1234,
			},
			ctxTime:   mustParseTime("2019-07-08T11:32:00Z"),
			ctxHeight: 1235,
			expected:  true,
		},
	}

	for name, tc := range cases {
		tc := tc // copy to local variable for scopelint
		t.Run(name, func(t *testing.T) {
			ctx := sdk.NewContext(nil, tmproto.Header{Height: tc.ctxHeight, Time: tc.ctxTime}, false, log.NewNopLogger())
			should := tc.p.ShouldExecute(ctx)
			assert.Equal(t, tc.expected, should)
		})
	}
}
