package solomachine_test

import (
	"errors"
	"fmt"

	codectypes "git.cw.tr/mukan-network/mukan-sdk/codec/types"
	sdk "git.cw.tr/mukan-network/mukan-sdk/types"

	clienttypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/02-client/types"
	channeltypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/04-channel/types"
	commitmenttypesv2 "git.cw.tr/mukan-network/mukan-ibc/modules/core/23-commitment/types/v2"
	host "git.cw.tr/mukan-network/mukan-ibc/modules/core/24-host"
	ibcerrors "git.cw.tr/mukan-network/mukan-ibc/modules/core/errors"
	"git.cw.tr/mukan-network/mukan-ibc/modules/core/exported"
	solomachine "git.cw.tr/mukan-network/mukan-ibc/modules/light-clients/06-solomachine"
	ibctm "git.cw.tr/mukan-network/mukan-ibc/modules/light-clients/07-tendermint"
	ibctesting "git.cw.tr/mukan-network/mukan-ibc/testing"
	ibcmock "git.cw.tr/mukan-network/mukan-ibc/testing/mock"
)

const (
	unusedSmClientID = "06-solomachine-999"
	wasmClientID     = "08-wasm-0"
)

func (suite *SoloMachineTestSuite) TestStatus() {
	var (
		clientState *solomachine.ClientState
		clientID    string
	)

	testCases := []struct {
		name      string
		malleate  func()
		expStatus exported.Status
	}{
		{
			"client is active",
			func() {},
			exported.Active,
		},
		{
			"client is frozen",
			func() {
				clientState = solomachine.NewClientState(0, &solomachine.ConsensusState{})
				clientState.IsFrozen = true
				suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState)
			},
			exported.Frozen,
		},
		{
			"failure: cannot find client state",
			func() {
				clientID = unusedSmClientID
			},
			exported.Unknown,
		},
	}

	for _, tc := range testCases {
		suite.Run(tc.name, func() {
			clientID = suite.solomachine.ClientID

			lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID)
			suite.Require().NoError(err)

			suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, suite.solomachine.ClientState())

			tc.malleate()

			status := lightClientModule.Status(suite.chainA.GetContext(), clientID)
			suite.Require().Equal(tc.expStatus, status)
		})
	}
}

func (suite *SoloMachineTestSuite) TestGetTimestampAtHeight() {
	var (
		clientID string
		height   exported.Height
	)

	testCases := []struct {
		name     string
		malleate func()
		expValue uint64
		expErr   error
	}{
		{
			"success: get timestamp at height exists",
			func() {},
			suite.solomachine.ClientState().ConsensusState.Timestamp,
			nil,
		},
		{
			"success: modified height",
			func() {
				height = clienttypes.ZeroHeight()
			},
			// Timestamp should be the same.
			suite.solomachine.ClientState().ConsensusState.Timestamp,
			nil,
		},
		{
			"failure: cannot find client state",
			func() {
				clientID = unusedSmClientID
			},
			0,
			clienttypes.ErrClientNotFound,
		},
	}

	for _, tc := range testCases {
		suite.Run(tc.name, func() {
			clientID = suite.solomachine.ClientID
			clientState := suite.solomachine.ClientState()
			height = clienttypes.NewHeight(0, suite.solomachine.ClientState().Sequence)

			lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID)
			suite.Require().NoError(err)

			suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState)

			tc.malleate()

			ts, err := lightClientModule.TimestampAtHeight(suite.chainA.GetContext(), clientID, height)

			suite.Require().Equal(tc.expValue, ts)
			suite.Require().ErrorIs(err, tc.expErr)
		})
	}
}

func (suite *SoloMachineTestSuite) TestInitialize() {
	// test singlesig and multisig public keys
	for _, sm := range []*ibctesting.Solomachine{suite.solomachine, suite.solomachineMulti} {
		malleatedConsensus := sm.ClientState().ConsensusState
		malleatedConsensus.Timestamp += 10

		testCases := []struct {
			name        string
			consState   exported.ConsensusState
			clientState exported.ClientState
			expErr      error
		}{
			{
				"success: valid consensus state",
				sm.ConsensusState(),
				sm.ClientState(),
				nil,
			},
			{
				"failure: nil consensus state",
				nil,
				sm.ClientState(),
				clienttypes.ErrInvalidConsensus,
			},
			{
				"failure: invalid consensus state: Tendermint consensus state",
				&ibctm.ConsensusState{},
				sm.ClientState(),
				errors.New("proto: wrong wireType = 0 for field TypeUrl"),
			},
			{
				"failure: invalid consensus state: consensus state does not match consensus state in client",
				malleatedConsensus,
				sm.ClientState(),
				clienttypes.ErrInvalidConsensus,
			},
			{
				"failure: invalid client state: sequence is zero",
				sm.ConsensusState(),
				solomachine.NewClientState(0, sm.ConsensusState()),
				clienttypes.ErrInvalidClient,
			},
			{
				"failure: invalid client state: Tendermint client state",
				sm.ConsensusState(),
				&ibctm.ClientState{},
				errors.New("proto: wrong wireType = 2 for field IsFrozen"),
			},
		}

		for _, tc := range testCases {
			suite.Run(tc.name, func() {
				suite.SetupTest()
				clientID := sm.ClientID

				clientStateBz := suite.chainA.Codec.MustMarshal(tc.clientState)
				consStateBz := suite.chainA.Codec.MustMarshal(tc.consState)

				lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID)
				suite.Require().NoError(err)

				err = lightClientModule.Initialize(suite.chainA.GetContext(), clientID, clientStateBz, consStateBz)
				store := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), clientID)

				if tc.expErr == nil {
					suite.Require().NoError(err)
					suite.Require().True(store.Has(host.ClientStateKey()))
				} else {
					suite.Require().ErrorContains(err, tc.expErr.Error())
					suite.Require().False(store.Has(host.ClientStateKey()))
				}
			})
		}
	}
}

func (suite *SoloMachineTestSuite) TestVerifyMembership() {
	var (
		clientState *solomachine.ClientState
		path        exported.Path
		proof       []byte
		testingPath *ibctesting.Path
		signBytes   solomachine.SignBytes
		err         error
		clientID    string
	)

	// test singlesig and multisig public keys
	for _, sm := range []*ibctesting.Solomachine{suite.solomachine, suite.solomachineMulti} {

		testCases := []struct {
			name     string
			malleate func()
			expErr   error
		}{
			{
				"success",
				func() {},
				nil,
			},
			{
				"success: client state verification",
				func() {
					clientState = sm.ClientState()
					clientStateBz, err := suite.chainA.Codec.MarshalInterface(clientState)
					suite.Require().NoError(err)

					path = sm.GetClientStatePath(counterpartyClientIdentifier)
					merklePath, ok := path.(commitmenttypesv2.MerklePath)
					suite.Require().True(ok)
					key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store
					suite.Require().NoError(err)
					signBytes = solomachine.SignBytes{
						Sequence:    sm.GetHeight().GetRevisionHeight(),
						Timestamp:   sm.Time,
						Diversifier: sm.Diversifier,
						Path:        key,
						Data:        clientStateBz,
					}

					signBz, err := suite.chainA.Codec.Marshal(&signBytes)
					suite.Require().NoError(err)

					sig := sm.GenerateSignature(signBz)

					signatureDoc := &solomachine.TimestampedSignatureData{
						SignatureData: sig,
						Timestamp:     sm.Time,
					}

					proof, err = suite.chainA.Codec.Marshal(signatureDoc)
					suite.Require().NoError(err)
				},
				nil,
			},
			{
				"success: consensus state verification",
				func() {
					clientState = sm.ClientState()
					consensusState := clientState.ConsensusState
					consensusStateBz, err := suite.chainA.Codec.MarshalInterface(consensusState)
					suite.Require().NoError(err)

					path = sm.GetConsensusStatePath(counterpartyClientIdentifier, clienttypes.NewHeight(0, 1))
					merklePath, ok := path.(commitmenttypesv2.MerklePath)
					suite.Require().True(ok)
					key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store
					suite.Require().NoError(err)
					signBytes = solomachine.SignBytes{
						Sequence:    sm.Sequence,
						Timestamp:   sm.Time,
						Diversifier: sm.Diversifier,
						Path:        key,
						Data:        consensusStateBz,
					}

					signBz, err := suite.chainA.Codec.Marshal(&signBytes)
					suite.Require().NoError(err)

					sig := sm.GenerateSignature(signBz)

					signatureDoc := &solomachine.TimestampedSignatureData{
						SignatureData: sig,
						Timestamp:     sm.Time,
					}

					proof, err = suite.chainA.Codec.Marshal(signatureDoc)
					suite.Require().NoError(err)
				},
				nil,
			},
			{
				"success: connection state verification",
				func() {
					testingPath.SetupConnections()

					connectionEnd, found := suite.chainA.GetSimApp().IBCKeeper.ConnectionKeeper.GetConnection(suite.chainA.GetContext(), ibctesting.FirstConnectionID)
					suite.Require().True(found)

					connectionEndBz, err := suite.chainA.Codec.Marshal(&connectionEnd)
					suite.Require().NoError(err)

					path = sm.GetConnectionStatePath(ibctesting.FirstConnectionID)
					merklePath, ok := path.(commitmenttypesv2.MerklePath)
					suite.Require().True(ok)
					key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store
					suite.Require().NoError(err)
					signBytes = solomachine.SignBytes{
						Sequence:    sm.Sequence,
						Timestamp:   sm.Time,
						Diversifier: sm.Diversifier,
						Path:        key,
						Data:        connectionEndBz,
					}

					signBz, err := suite.chainA.Codec.Marshal(&signBytes)
					suite.Require().NoError(err)

					sig := sm.GenerateSignature(signBz)

					signatureDoc := &solomachine.TimestampedSignatureData{
						SignatureData: sig,
						Timestamp:     sm.Time,
					}

					proof, err = suite.chainA.Codec.Marshal(signatureDoc)
					suite.Require().NoError(err)
				},
				nil,
			},
			{
				"success: channel state verification",
				func() {
					testingPath.SetupConnections()
					suite.coordinator.CreateMockChannels(testingPath)

					channelEnd, found := suite.chainA.GetSimApp().IBCKeeper.ChannelKeeper.GetChannel(suite.chainA.GetContext(), ibctesting.MockPort, testingPath.EndpointA.ChannelID)
					suite.Require().True(found)

					channelEndBz, err := suite.chainA.Codec.Marshal(&channelEnd)
					suite.Require().NoError(err)

					path = sm.GetChannelStatePath(ibctesting.MockPort, ibctesting.FirstChannelID)
					merklePath, ok := path.(commitmenttypesv2.MerklePath)
					suite.Require().True(ok)
					key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store
					suite.Require().NoError(err)
					signBytes = solomachine.SignBytes{
						Sequence:    sm.Sequence,
						Timestamp:   sm.Time,
						Diversifier: sm.Diversifier,
						Path:        key,
						Data:        channelEndBz,
					}

					signBz, err := suite.chainA.Codec.Marshal(&signBytes)
					suite.Require().NoError(err)

					sig := sm.GenerateSignature(signBz)

					signatureDoc := &solomachine.TimestampedSignatureData{
						SignatureData: sig,
						Timestamp:     sm.Time,
					}

					proof, err = suite.chainA.Codec.Marshal(signatureDoc)
					suite.Require().NoError(err)
				},
				nil,
			},
			{
				"success: next sequence recv verification",
				func() {
					testingPath.SetupConnections()
					suite.coordinator.CreateMockChannels(testingPath)

					nextSeqRecv, found := suite.chainA.GetSimApp().IBCKeeper.ChannelKeeper.GetNextSequenceRecv(suite.chainA.GetContext(), ibctesting.MockPort, testingPath.EndpointA.ChannelID)
					suite.Require().True(found)

					path = sm.GetNextSequenceRecvPath(ibctesting.MockPort, ibctesting.FirstChannelID)
					merklePath, ok := path.(commitmenttypesv2.MerklePath)
					suite.Require().True(ok)
					key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store
					suite.Require().NoError(err)
					signBytes = solomachine.SignBytes{
						Sequence:    sm.Sequence,
						Timestamp:   sm.Time,
						Diversifier: sm.Diversifier,
						Path:        key,
						Data:        sdk.Uint64ToBigEndian(nextSeqRecv),
					}

					signBz, err := suite.chainA.Codec.Marshal(&signBytes)
					suite.Require().NoError(err)

					sig := sm.GenerateSignature(signBz)

					signatureDoc := &solomachine.TimestampedSignatureData{
						SignatureData: sig,
						Timestamp:     sm.Time,
					}

					proof, err = suite.chainA.Codec.Marshal(signatureDoc)
					suite.Require().NoError(err)
				},
				nil,
			},
			{
				"success: packet commitment verification",
				func() {
					packet := channeltypes.NewPacket(
						ibctesting.MockPacketData,
						1,
						ibctesting.MockPort,
						ibctesting.FirstChannelID,
						ibctesting.MockPort,
						ibctesting.FirstChannelID,
						clienttypes.NewHeight(0, 10),
						0,
					)

					commitmentBz := channeltypes.CommitPacket(packet)
					path = sm.GetPacketCommitmentPath(packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())
					merklePath, ok := path.(commitmenttypesv2.MerklePath)
					suite.Require().True(ok)
					key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store
					suite.Require().NoError(err)
					signBytes = solomachine.SignBytes{
						Sequence:    sm.Sequence,
						Timestamp:   sm.Time,
						Diversifier: sm.Diversifier,
						Path:        key,
						Data:        commitmentBz,
					}

					signBz, err := suite.chainA.Codec.Marshal(&signBytes)
					suite.Require().NoError(err)

					sig := sm.GenerateSignature(signBz)

					signatureDoc := &solomachine.TimestampedSignatureData{
						SignatureData: sig,
						Timestamp:     sm.Time,
					}

					proof, err = suite.chainA.Codec.Marshal(signatureDoc)
					suite.Require().NoError(err)
				},
				nil,
			},
			{
				"success: packet acknowledgement verification",
				func() {
					path = sm.GetPacketAcknowledgementPath(ibctesting.MockPort, ibctesting.FirstChannelID, 1)
					merklePath, ok := path.(commitmenttypesv2.MerklePath)
					suite.Require().True(ok)
					key, err := merklePath.GetKey(1) // index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store
					suite.Require().NoError(err)
					signBytes = solomachine.SignBytes{
						Sequence:    sm.Sequence,
						Timestamp:   sm.Time,
						Diversifier: sm.Diversifier,
						Path:        key,
						Data:        ibctesting.MockAcknowledgement,
					}

					signBz, err := suite.chainA.Codec.Marshal(&signBytes)
					suite.Require().NoError(err)

					sig := sm.GenerateSignature(signBz)

					signatureDoc := &solomachine.TimestampedSignatureData{
						SignatureData: sig,
						Timestamp:     sm.Time,
					}

					proof, err = suite.chainA.Codec.Marshal(signatureDoc)
					suite.Require().NoError(err)
				},
				nil,
			},
			{
				"success: packet receipt verification",
				func() {
					path = sm.GetPacketReceiptPath(ibctesting.MockPort, ibctesting.FirstChannelID, 1)
					merklePath, ok := path.(commitmenttypesv2.MerklePath)
					suite.Require().True(ok)
					key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store
					suite.Require().NoError(err)
					signBytes = solomachine.SignBytes{
						Sequence:    sm.Sequence,
						Timestamp:   sm.Time,
						Diversifier: sm.Diversifier,
						Path:        key,
						Data:        []byte{byte(1)}, // packet receipt is stored as a single byte
					}

					signBz, err := suite.chainA.Codec.Marshal(&signBytes)
					suite.Require().NoError(err)

					sig := sm.GenerateSignature(signBz)

					signatureDoc := &solomachine.TimestampedSignatureData{
						SignatureData: sig,
						Timestamp:     sm.Time,
					}

					proof, err = suite.chainA.Codec.Marshal(signatureDoc)
					suite.Require().NoError(err)
				},
				nil,
			},
			{
				"failure: cannot find client state",
				func() {
					clientID = unusedSmClientID
				},
				clienttypes.ErrClientNotFound,
			},
			{
				"failure: invalid path type - empty",
				func() {
					path = ibcmock.KeyPath{}
				},
				ibcerrors.ErrInvalidType,
			},
			{
				"failure: malformed proof fails to unmarshal",
				func() {
					path = sm.GetClientStatePath(counterpartyClientIdentifier)
					proof = []byte("invalid proof")
				},
				errors.New("failed to unmarshal proof into type"),
			},
			{
				"failure: consensus state timestamp is greater than signature",
				func() {
					consensusState := &solomachine.ConsensusState{
						Timestamp: sm.Time + 1,
						PublicKey: sm.ConsensusState().PublicKey,
					}

					clientState = solomachine.NewClientState(sm.Sequence, consensusState)
					suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState)
				},
				fmt.Errorf("the consensus state timestamp is greater than the signature timestamp (11 >= 10): %s", solomachine.ErrInvalidProof),
			},
			{
				"failure: signature data is nil",
				func() {
					signatureDoc := &solomachine.TimestampedSignatureData{
						SignatureData: nil,
						Timestamp:     sm.Time,
					}

					proof, err = suite.chainA.Codec.Marshal(signatureDoc)
					suite.Require().NoError(err)
				},
				fmt.Errorf("signature data cannot be empty: %s", solomachine.ErrInvalidProof),
			},
			{
				"failure: consensus state public key is nil",
				func() {
					clientState.ConsensusState.PublicKey = nil
					suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState)
				},
				fmt.Errorf("consensus state PublicKey cannot be nil: %s", clienttypes.ErrInvalidConsensus),
			},
			{
				"failure: malformed signature data fails to unmarshal",
				func() {
					signatureDoc := &solomachine.TimestampedSignatureData{
						SignatureData: []byte("invalid signature data"),
						Timestamp:     sm.Time,
					}

					proof, err = suite.chainA.Codec.Marshal(signatureDoc)
					suite.Require().NoError(err)
				},
				errors.New("failed to unmarshal proof into type"),
			},
			{
				"failure: proof is nil",
				func() {
					proof = nil
				},
				fmt.Errorf("proof cannot be empty: %s", solomachine.ErrInvalidProof),
			},
			{
				"failure: proof verification failed",
				func() {
					signBytes.Data = []byte("invalid membership data value")
				},
				solomachine.ErrSignatureVerificationFailed,
			},
			{
				"failure: empty path",
				func() {
					path = commitmenttypesv2.MerklePath{}
				},
				fmt.Errorf("path must be of length 2: []: %s", host.ErrInvalidPath),
			},
		}

		for _, tc := range testCases {
			suite.Run(tc.name, func() {
				suite.SetupTest()
				testingPath = ibctesting.NewPath(suite.chainA, suite.chainB)

				clientID = sm.ClientID
				clientState = sm.ClientState()

				path = commitmenttypesv2.NewMerklePath([]byte("ibc"), []byte("solomachine"))
				merklePath, ok := path.(commitmenttypesv2.MerklePath)
				suite.Require().True(ok)
				key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store
				suite.Require().NoError(err)
				signBytes = solomachine.SignBytes{
					Sequence:    sm.GetHeight().GetRevisionHeight(),
					Timestamp:   sm.Time,
					Diversifier: sm.Diversifier,
					Path:        key,
					Data:        []byte("solomachine"),
				}

				signBz, err := suite.chainA.Codec.Marshal(&signBytes)
				suite.Require().NoError(err)

				sig := sm.GenerateSignature(signBz)

				signatureDoc := &solomachine.TimestampedSignatureData{
					SignatureData: sig,
					Timestamp:     sm.Time,
				}

				proof, err = suite.chainA.Codec.Marshal(signatureDoc)
				suite.Require().NoError(err)

				lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID)
				suite.Require().NoError(err)

				// Set the client state in the store for light client call to find.
				suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState)

				tc.malleate()

				var expSeq uint64
				if clientState.ConsensusState != nil {
					expSeq = clientState.Sequence + 1
				}

				// Verify the membership proof
				err = lightClientModule.VerifyMembership(
					suite.chainA.GetContext(), clientID, clienttypes.ZeroHeight(),
					0, 0, proof, path, signBytes.Data,
				)

				if tc.expErr == nil {
					// Grab fresh client state after updates.
					cs, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.GetClientState(suite.chainA.GetContext(), clientID)
					suite.Require().True(found)
					clientState, ok = cs.(*solomachine.ClientState)
					suite.Require().True(ok)

					suite.Require().NoError(err)
					// clientState.Sequence is the most recent view of state.
					suite.Require().Equal(expSeq, clientState.Sequence)
				} else {
					suite.Require().Error(err)
					suite.Require().ErrorContains(err, tc.expErr.Error())
				}
			})
		}
	}
}

func (suite *SoloMachineTestSuite) TestVerifyNonMembership() {
	var (
		clientState *solomachine.ClientState
		path        exported.Path
		proof       []byte
		signBytes   solomachine.SignBytes
		err         error
		clientID    string
	)

	// test singlesig and multisig public keys
	for _, sm := range []*ibctesting.Solomachine{suite.solomachine, suite.solomachineMulti} {
		testCases := []struct {
			name     string
			malleate func()
			expErr   error
		}{
			{
				"success",
				func() {},
				nil,
			},
			{
				"success: packet receipt absence verification",
				func() {
					path = sm.GetPacketReceiptPath(ibctesting.MockPort, ibctesting.FirstChannelID, 1)
					merklePath, ok := path.(commitmenttypesv2.MerklePath)
					suite.Require().True(ok)
					key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store
					suite.Require().NoError(err)
					signBytes = solomachine.SignBytes{
						Sequence:    sm.GetHeight().GetRevisionHeight(),
						Timestamp:   sm.Time,
						Diversifier: sm.Diversifier,
						Path:        key,
						Data:        nil,
					}

					signBz, err := suite.chainA.Codec.Marshal(&signBytes)
					suite.Require().NoError(err)

					sig := sm.GenerateSignature(signBz)

					signatureDoc := &solomachine.TimestampedSignatureData{
						SignatureData: sig,
						Timestamp:     sm.Time,
					}

					proof, err = suite.chainA.Codec.Marshal(signatureDoc)
					suite.Require().NoError(err)
				},
				nil,
			},
			{
				"failure: cannot find client state",
				func() {
					clientID = unusedSmClientID
				},
				clienttypes.ErrClientNotFound,
			},
			{
				"failure: invalid path type",
				func() {
					path = ibcmock.KeyPath{}
				},
				ibcerrors.ErrInvalidType,
			},
			{
				"failure: malformed proof fails to unmarshal",
				func() {
					path = sm.GetClientStatePath(counterpartyClientIdentifier)
					proof = []byte("invalid proof")
				},
				errors.New("failed to unmarshal proof into type"),
			},
			{
				"failure: consensus state timestamp is greater than signature",
				func() {
					consensusState := &solomachine.ConsensusState{
						Timestamp: sm.Time + 1,
						PublicKey: sm.ConsensusState().PublicKey,
					}

					clientState = solomachine.NewClientState(sm.Sequence, consensusState)
					suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState)
				},
				fmt.Errorf("the consensus state timestamp is greater than the signature timestamp (11 >= 10): %s", solomachine.ErrInvalidProof),
			},
			{
				"failure: signature data is nil",
				func() {
					signatureDoc := &solomachine.TimestampedSignatureData{
						SignatureData: nil,
						Timestamp:     sm.Time,
					}

					proof, err = suite.chainA.Codec.Marshal(signatureDoc)
					suite.Require().NoError(err)
				},
				fmt.Errorf("signature data cannot be empty: %s", solomachine.ErrInvalidProof),
			},
			{
				"failure: consensus state public key is nil",
				func() {
					clientState.ConsensusState.PublicKey = nil
					suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState)
				},
				fmt.Errorf("consensus state PublicKey cannot be nil: %s", clienttypes.ErrInvalidConsensus),
			},
			{
				"failure: malformed signature data fails to unmarshal",
				func() {
					signatureDoc := &solomachine.TimestampedSignatureData{
						SignatureData: []byte("invalid signature data"),
						Timestamp:     sm.Time,
					}

					proof, err = suite.chainA.Codec.Marshal(signatureDoc)
					suite.Require().NoError(err)
				},
				errors.New("failed to unmarshal proof into type"),
			},
			{
				"failure: proof is nil",
				func() {
					proof = nil
				},
				fmt.Errorf("proof cannot be empty: %s", solomachine.ErrInvalidProof),
			},
			{
				"failure: proof verification failed",
				func() {
					signBytes.Data = []byte("invalid non-membership data value")

					signBz, err := suite.chainA.Codec.Marshal(&signBytes)
					suite.Require().NoError(err)

					sig := sm.GenerateSignature(signBz)

					signatureDoc := &solomachine.TimestampedSignatureData{
						SignatureData: sig,
						Timestamp:     sm.Time,
					}

					proof, err = suite.chainA.Codec.Marshal(signatureDoc)
					suite.Require().NoError(err)
				},
				solomachine.ErrSignatureVerificationFailed,
			},
		}

		for _, tc := range testCases {
			suite.Run(tc.name, func() {
				suite.SetupTest()

				clientState = sm.ClientState()
				clientID = sm.ClientID

				path = commitmenttypesv2.NewMerklePath([]byte("ibc"), []byte("solomachine"))
				merklePath, ok := path.(commitmenttypesv2.MerklePath)
				suite.Require().True(ok)
				key, err := merklePath.GetKey(1) // in a multistore context: index 0 is the key for the IBC store in the multistore, index 1 is the key in the IBC store
				suite.Require().NoError(err)
				signBytes = solomachine.SignBytes{
					Sequence:    sm.GetHeight().GetRevisionHeight(),
					Timestamp:   sm.Time,
					Diversifier: sm.Diversifier,
					Path:        key,
					Data:        nil,
				}

				signBz, err := suite.chainA.Codec.Marshal(&signBytes)
				suite.Require().NoError(err)

				sig := sm.GenerateSignature(signBz)

				signatureDoc := &solomachine.TimestampedSignatureData{
					SignatureData: sig,
					Timestamp:     sm.Time,
				}

				proof, err = suite.chainA.Codec.Marshal(signatureDoc)
				suite.Require().NoError(err)

				lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID)
				suite.Require().NoError(err)

				// Set the client state in the store for light client call to find.
				suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState)

				tc.malleate()

				var expSeq uint64
				if clientState.ConsensusState != nil {
					expSeq = clientState.Sequence + 1
				}

				// Verify the membership proof
				err = lightClientModule.VerifyNonMembership(
					suite.chainA.GetContext(), clientID, clienttypes.ZeroHeight(),
					0, 0, proof, path,
				)

				if tc.expErr == nil {
					// Grab fresh client state after updates.
					cs, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.GetClientState(suite.chainA.GetContext(), clientID)
					suite.Require().True(found)
					clientState, ok = cs.(*solomachine.ClientState)
					suite.Require().True(ok)

					suite.Require().NoError(err)
					suite.Require().Equal(expSeq, clientState.Sequence)
				} else {
					suite.Require().Error(err)
					suite.Require().ErrorContains(err, tc.expErr.Error())
				}
			})
		}
	}
}

func (suite *SoloMachineTestSuite) TestRecoverClient() {
	var (
		subjectClientID, substituteClientID       string
		subjectClientState, substituteClientState *solomachine.ClientState
	)

	testCases := []struct {
		name     string
		malleate func()
		expErr   error
	}{
		{
			"success",
			func() {
			},
			nil,
		},
		{
			"failure: cannot parse malformed substitute client ID",
			func() {
				substituteClientID = ibctesting.InvalidID
			},
			host.ErrInvalidID,
		},
		{
			"failure: substitute client ID does not contain 06-solomachine prefix",
			func() {
				substituteClientID = wasmClientID
			},
			clienttypes.ErrInvalidClientType,
		},
		{
			"failure: cannot find subject client state",
			func() {
				subjectClientID = unusedSmClientID
			},
			clienttypes.ErrClientNotFound,
		},
		{
			"failure: cannot find substitute client state",
			func() {
				substituteClientID = unusedSmClientID
			},
			clienttypes.ErrClientNotFound,
		},
	}

	for _, tc := range testCases {
		suite.Run(tc.name, func() {
			suite.SetupTest() // reset

			ctx := suite.chainA.GetContext()

			subjectClientID = suite.chainA.App.GetIBCKeeper().ClientKeeper.GenerateClientIdentifier(ctx, exported.Solomachine)
			subject := ibctesting.NewSolomachine(suite.T(), suite.chainA.Codec, substituteClientID, "testing", 1)
			subjectClientState = subject.ClientState()

			substituteClientID = suite.chainA.App.GetIBCKeeper().ClientKeeper.GenerateClientIdentifier(ctx, exported.Solomachine)
			substitute := ibctesting.NewSolomachine(suite.T(), suite.chainA.Codec, substituteClientID, "testing", 1)
			substitute.Sequence++ // increase sequence so that latest height of substitute is > than subject's latest height
			substituteClientState = substitute.ClientState()

			clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(ctx, substituteClientID)
			clientStore.Get(host.ClientStateKey())
			bz := clienttypes.MustMarshalClientState(suite.chainA.Codec, substituteClientState)
			clientStore.Set(host.ClientStateKey(), bz)

			subjectClientState.IsFrozen = true
			suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(ctx, subjectClientID, subjectClientState)

			lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), subjectClientID)
			suite.Require().NoError(err)

			tc.malleate()

			err = lightClientModule.RecoverClient(ctx, subjectClientID, substituteClientID)

			if tc.expErr == nil {
				suite.Require().NoError(err)

				// assert that status of subject client is now Active
				clientStore = suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(ctx, subjectClientID)
				bz = clientStore.Get(host.ClientStateKey())
				smClientState, ok := clienttypes.MustUnmarshalClientState(suite.chainA.Codec, bz).(*solomachine.ClientState)
				suite.Require().True(ok)

				suite.Require().Equal(substituteClientState.ConsensusState, smClientState.ConsensusState)
				suite.Require().Equal(substituteClientState.Sequence, smClientState.Sequence)
				suite.Require().Equal(exported.Active, lightClientModule.Status(ctx, subjectClientID))
			} else {
				suite.Require().Error(err)
				suite.Require().ErrorIs(err, tc.expErr)
			}
		})
	}
}

func (suite *SoloMachineTestSuite) TestUpdateState() {
	var (
		clientState *solomachine.ClientState
		clientMsg   exported.ClientMessage
		clientID    string
	)

	// test singlesig and multisig public keys
	for _, sm := range []*ibctesting.Solomachine{suite.solomachine, suite.solomachineMulti} {

		testCases := []struct {
			name     string
			malleate func()
			expPanic error
		}{
			{
				"successful update",
				func() {},
				nil,
			},
			{
				"invalid type misbehaviour no-ops",
				func() {
					clientState = sm.ClientState()
					clientMsg = sm.CreateMisbehaviour()
					suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState)
				},
				nil,
			},
			{
				"failure: cannot find client state",
				func() {
					clientID = unusedSmClientID
				},
				fmt.Errorf("%s: %s", unusedSmClientID, clienttypes.ErrClientNotFound),
			},
		}

		for _, tc := range testCases {
			suite.Run(tc.name, func() {
				suite.SetupTest()

				clientID = sm.ClientID
				clientState = sm.ClientState()
				clientMsg = sm.CreateHeader(sm.Diversifier)

				lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID)
				suite.Require().NoError(err)

				suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState)

				tc.malleate() // setup test

				store := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), clientID)

				var consensusHeights []exported.Height
				updateStateFunc := func() {
					consensusHeights = lightClientModule.UpdateState(suite.chainA.GetContext(), clientID, clientMsg)
				}

				if tc.expPanic == nil {
					updateStateFunc()

					clientStateBz := store.Get(host.ClientStateKey())
					suite.Require().NotEmpty(clientStateBz)

					newClientState := clienttypes.MustUnmarshalClientState(suite.chainA.Codec, clientStateBz)

					if len(consensusHeights) == 0 {
						suite.Require().Equal(clientState, newClientState)
						return
					}

					suite.Require().Len(consensusHeights, 1)
					suite.Require().Equal(uint64(0), consensusHeights[0].GetRevisionNumber())
					suite.Require().Equal(newClientState.(*solomachine.ClientState).Sequence, consensusHeights[0].GetRevisionHeight())

					suite.Require().False(newClientState.(*solomachine.ClientState).IsFrozen)
					suite.Require().Equal(clientMsg.(*solomachine.Header).NewPublicKey, newClientState.(*solomachine.ClientState).ConsensusState.PublicKey)
					suite.Require().Equal(clientMsg.(*solomachine.Header).NewDiversifier, newClientState.(*solomachine.ClientState).ConsensusState.Diversifier)
					suite.Require().Equal(clientMsg.(*solomachine.Header).Timestamp, newClientState.(*solomachine.ClientState).ConsensusState.Timestamp)
				} else {
					suite.Require().PanicsWithError(tc.expPanic.Error(), updateStateFunc)
				}
			})
		}
	}
}

func (suite *SoloMachineTestSuite) TestCheckForMisbehaviour() {
	var (
		clientMsg exported.ClientMessage
		clientID  string
	)

	// test singlesig and multisig public keys
	for _, sm := range []*ibctesting.Solomachine{suite.solomachine, suite.solomachineMulti} {
		testCases := []struct {
			name              string
			malleate          func()
			foundMisbehaviour bool
			expPanic          error
		}{
			{
				"success",
				func() {
					clientMsg = sm.CreateMisbehaviour()
				},
				true,
				nil,
			},
			{
				"failure: normal header returns false",
				func() {
					clientMsg = sm.CreateHeader(sm.Diversifier)
				},
				false,
				nil,
			},
			{
				"failure: cannot find client state",
				func() {
					clientID = unusedSmClientID
				},
				false,
				fmt.Errorf("%s: %s", unusedSmClientID, clienttypes.ErrClientNotFound),
			},
		}

		for _, tc := range testCases {
			suite.Run(tc.name, func() {
				suite.SetupTest()

				clientID = sm.ClientID

				lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID)
				suite.Require().NoError(err)

				suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, sm.ClientState())

				tc.malleate()

				var foundMisbehaviour bool
				foundMisbehaviourFunc := func() {
					foundMisbehaviour = lightClientModule.CheckForMisbehaviour(suite.chainA.GetContext(), clientID, clientMsg)
				}

				if tc.expPanic == nil {
					foundMisbehaviourFunc()

					suite.Require().Equal(tc.foundMisbehaviour, foundMisbehaviour)
				} else {
					suite.Require().PanicsWithError(tc.expPanic.Error(), foundMisbehaviourFunc)
					suite.Require().False(foundMisbehaviour)
				}
			})
		}
	}
}

func (suite *SoloMachineTestSuite) TestUpdateStateOnMisbehaviour() {
	var clientID string

	// test singlesig and multisig public keys
	for _, sm := range []*ibctesting.Solomachine{suite.solomachine, suite.solomachineMulti} {
		testCases := []struct {
			name     string
			malleate func()
			expPanic error
		}{
			{
				"success",
				func() {},
				nil,
			},
			{
				"failure: cannot find client state",
				func() {
					clientID = unusedSmClientID
				},
				fmt.Errorf("%s: %s", unusedSmClientID, clienttypes.ErrClientNotFound),
			},
		}

		for _, tc := range testCases {
			suite.Run(tc.name, func() {
				suite.SetupTest()
				clientID = sm.ClientID

				lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID)
				suite.Require().NoError(err)

				suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, sm.ClientState())

				tc.malleate()

				updateOnMisbehaviourFunc := func() {
					lightClientModule.UpdateStateOnMisbehaviour(suite.chainA.GetContext(), clientID, nil)
				}

				if tc.expPanic == nil {
					updateOnMisbehaviourFunc()

					store := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), clientID)

					clientStateBz := store.Get(host.ClientStateKey())
					suite.Require().NotEmpty(clientStateBz)

					newClientState := clienttypes.MustUnmarshalClientState(suite.chainA.Codec, clientStateBz)

					suite.Require().True(newClientState.(*solomachine.ClientState).IsFrozen)
				} else {
					suite.Require().PanicsWithError(tc.expPanic.Error(), updateOnMisbehaviourFunc)
				}
			})
		}
	}
}

func (suite *SoloMachineTestSuite) TestVerifyClientMessageHeader() {
	var (
		clientID  string
		clientMsg exported.ClientMessage
	)

	// test singlesig and multisig public keys
	for _, sm := range []*ibctesting.Solomachine{suite.solomachine, suite.solomachineMulti} {

		testCases := []struct {
			name     string
			malleate func()
			expErr   error
		}{
			{
				"success: successful header",
				func() {
					clientMsg = sm.CreateHeader(sm.Diversifier)
				},
				nil,
			},
			{
				"success: successful header with new diversifier",
				func() {
					clientMsg = sm.CreateHeader(sm.Diversifier + "0")
				},
				nil,
			},
			{
				"success: successful misbehaviour",
				func() {
					clientMsg = sm.CreateMisbehaviour()
				},
				nil,
			},
			{
				"failure: invalid client message type",
				func() {
					clientMsg = &ibctm.Header{}
				},
				clienttypes.ErrInvalidClientType,
			},
			{
				"failure: invalid header Signature",
				func() {
					h := sm.CreateHeader(sm.Diversifier)
					h.Signature = suite.GetInvalidProof()
					clientMsg = h
				}, errors.New("proto: wrong wireType = 0 for field Multi"),
			},
			{
				"failure: invalid timestamp in header",
				func() {
					h := sm.CreateHeader(sm.Diversifier)
					h.Timestamp--
					clientMsg = h
				}, clienttypes.ErrInvalidHeader,
			},
			{
				"failure: signature uses wrong sequence",
				func() {
					sm.Sequence++
					clientMsg = sm.CreateHeader(sm.Diversifier)
				},
				solomachine.ErrSignatureVerificationFailed,
			},
			{
				"signature uses new pubkey to sign",
				func() {
					// store in temp before assigning to interface type
					cs := sm.ClientState()
					h := sm.CreateHeader(sm.Diversifier)

					publicKey, err := codectypes.NewAnyWithValue(sm.PublicKey)
					suite.NoError(err)

					data := &solomachine.HeaderData{
						NewPubKey:      publicKey,
						NewDiversifier: h.NewDiversifier,
					}

					dataBz, err := suite.chainA.Codec.Marshal(data)
					suite.Require().NoError(err)

					// generate invalid signature
					signBytes := &solomachine.SignBytes{
						Sequence:    cs.Sequence,
						Timestamp:   sm.Time,
						Diversifier: sm.Diversifier,
						Path:        []byte("invalid signature data"),
						Data:        dataBz,
					}

					signBz, err := suite.chainA.Codec.Marshal(signBytes)
					suite.Require().NoError(err)

					sig := sm.GenerateSignature(signBz)
					suite.Require().NoError(err)
					h.Signature = sig

					clientMsg = h
				},
				solomachine.ErrSignatureVerificationFailed,
			},
			{
				"failure: signature signs over old pubkey",
				func() {
					// store in temp before assigning to interface type
					cs := sm.ClientState()

					oldPubKey := sm.PublicKey
					h := sm.CreateHeader(sm.Diversifier)

					// generate invalid signature
					data := append(sdk.Uint64ToBigEndian(cs.Sequence), oldPubKey.Bytes()...)
					sig := sm.GenerateSignature(data)
					h.Signature = sig

					clientMsg = h
				},
				solomachine.ErrSignatureVerificationFailed,
			},
			{
				"failure: consensus state public key is nil - header",
				func() {
					h := sm.CreateHeader(sm.Diversifier)
					h.NewPublicKey = nil
					clientMsg = h
				},
				solomachine.ErrSignatureVerificationFailed,
			},
			{
				"failure: cannot find client state",
				func() {
					clientID = unusedSmClientID
				},
				fmt.Errorf("%s: %s", unusedSmClientID, clienttypes.ErrClientNotFound),
			},
		}

		for _, tc := range testCases {
			suite.Run(tc.name, func() {
				suite.SetupTest()
				clientID = sm.ClientID

				lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID)
				suite.Require().NoError(err)

				suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, sm.ClientState())

				tc.malleate()

				err = lightClientModule.VerifyClientMessage(suite.chainA.GetContext(), clientID, clientMsg)

				if tc.expErr == nil {
					suite.Require().NoError(err)
				} else {
					suite.Require().ErrorContains(err, tc.expErr.Error())
				}
			})
		}
	}
}

func (suite *SoloMachineTestSuite) TestVerifyClientMessageMisbehaviour() {
	var (
		clientMsg   exported.ClientMessage
		clientState *solomachine.ClientState
		clientID    string
	)

	// test singlesig and multisig public keys
	for _, sm := range []*ibctesting.Solomachine{suite.solomachine, suite.solomachineMulti} {

		testCases := []struct {
			name     string
			malleate func()
			expErr   error
		}{
			{
				"success: successful misbehaviour",
				func() {
					clientMsg = sm.CreateMisbehaviour()
				},
				nil,
			},
			{
				"success: old misbehaviour is successful (timestamp is less than current consensus state)",
				func() {
					clientState = sm.ClientState()
					sm.Time -= 5
					clientMsg = sm.CreateMisbehaviour()
				}, nil,
			},
			{
				"failure: invalid client message type",
				func() {
					clientMsg = &ibctm.Header{}
				},
				clienttypes.ErrInvalidClientType,
			},
			{
				"failure: consensus state pubkey is nil",
				func() {
					clientState.ConsensusState.PublicKey = nil
					clientMsg = sm.CreateMisbehaviour()
					suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState)
				},
				clienttypes.ErrInvalidConsensus,
			},
			{
				"failure: invalid SignatureOne SignatureData",
				func() {
					m := sm.CreateMisbehaviour()

					m.SignatureOne.Signature = suite.GetInvalidProof()
					clientMsg = m
				}, errors.New("proto: wrong wireType = 0 for field Multi"),
			},
			{
				"failure: invalid SignatureTwo SignatureData",
				func() {
					m := sm.CreateMisbehaviour()

					m.SignatureTwo.Signature = suite.GetInvalidProof()
					clientMsg = m
				}, errors.New("proto: wrong wireType = 0 for field Multi"),
			},
			{
				"failure: invalid SignatureOne timestamp",
				func() {
					m := sm.CreateMisbehaviour()

					m.SignatureOne.Timestamp = 1000000000000
					clientMsg = m
				}, solomachine.ErrSignatureVerificationFailed,
			},
			{
				"failure: invalid SignatureTwo timestamp",
				func() {
					m := sm.CreateMisbehaviour()

					m.SignatureTwo.Timestamp = 1000000000000
					clientMsg = m
				}, solomachine.ErrSignatureVerificationFailed,
			},
			{
				"failure: invalid first signature data",
				func() {
					// store in temp before assigning to interface type
					m := sm.CreateMisbehaviour()

					msg := []byte("DATA ONE")
					signBytes := &solomachine.SignBytes{
						Sequence:    sm.Sequence + 1,
						Timestamp:   sm.Time,
						Diversifier: sm.Diversifier,
						Path:        []byte("invalid signature data"),
						Data:        msg,
					}

					data, err := suite.chainA.Codec.Marshal(signBytes)
					suite.Require().NoError(err)

					sig := sm.GenerateSignature(data)

					m.SignatureOne.Signature = sig
					m.SignatureOne.Data = msg
					clientMsg = m
				},
				solomachine.ErrSignatureVerificationFailed,
			},
			{
				"failure: invalid second signature data",
				func() {
					// store in temp before assigning to interface type
					m := sm.CreateMisbehaviour()

					msg := []byte("DATA TWO")
					signBytes := &solomachine.SignBytes{
						Sequence:    sm.Sequence + 1,
						Timestamp:   sm.Time,
						Diversifier: sm.Diversifier,
						Path:        []byte("invalid signature data"),
						Data:        msg,
					}

					data, err := suite.chainA.Codec.Marshal(signBytes)
					suite.Require().NoError(err)

					sig := sm.GenerateSignature(data)

					m.SignatureTwo.Signature = sig
					m.SignatureTwo.Data = msg
					clientMsg = m
				},
				solomachine.ErrSignatureVerificationFailed,
			},
			{
				"failure: wrong pubkey generates first signature",
				func() {
					badMisbehaviour := sm.CreateMisbehaviour()

					// update public key to a new one
					sm.CreateHeader(sm.Diversifier)
					m := sm.CreateMisbehaviour()

					// set SignatureOne to use the wrong signature
					m.SignatureOne = badMisbehaviour.SignatureOne
					clientMsg = m
				}, solomachine.ErrSignatureVerificationFailed,
			},
			{
				"failure: wrong pubkey generates second signature",
				func() {
					badMisbehaviour := sm.CreateMisbehaviour()

					// update public key to a new one
					sm.CreateHeader(sm.Diversifier)
					m := sm.CreateMisbehaviour()

					// set SignatureTwo to use the wrong signature
					m.SignatureTwo = badMisbehaviour.SignatureTwo
					clientMsg = m
				}, solomachine.ErrSignatureVerificationFailed,
			},
			{
				"failure: signatures sign over different sequence",
				func() {
					// store in temp before assigning to interface type
					m := sm.CreateMisbehaviour()

					// Signature One
					msg := []byte("DATA ONE")
					// sequence used is plus 1
					signBytes := &solomachine.SignBytes{
						Sequence:    sm.Sequence + 1,
						Timestamp:   sm.Time,
						Diversifier: sm.Diversifier,
						Path:        []byte("invalid signature data"),
						Data:        msg,
					}

					data, err := suite.chainA.Codec.Marshal(signBytes)
					suite.Require().NoError(err)

					sig := sm.GenerateSignature(data)

					m.SignatureOne.Signature = sig
					m.SignatureOne.Data = msg

					// Signature Two
					msg = []byte("DATA TWO")
					// sequence used is minus 1

					signBytes = &solomachine.SignBytes{
						Sequence:    sm.Sequence - 1,
						Timestamp:   sm.Time,
						Diversifier: sm.Diversifier,
						Path:        []byte("invalid signature data"),
						Data:        msg,
					}
					data, err = suite.chainA.Codec.Marshal(signBytes)
					suite.Require().NoError(err)

					sig = sm.GenerateSignature(data)

					m.SignatureTwo.Signature = sig
					m.SignatureTwo.Data = msg

					clientMsg = m
				},
				solomachine.ErrSignatureVerificationFailed,
			},
			{
				"failure: cannot find client state",
				func() {
					clientID = unusedSmClientID
				},
				fmt.Errorf("%s: %s", unusedSmClientID, clienttypes.ErrClientNotFound),
			},
		}

		for _, tc := range testCases {
			suite.Run(tc.name, func() {
				suite.SetupTest()
				clientID = sm.ClientID

				lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID)
				suite.Require().NoError(err)

				suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, sm.ClientState())

				tc.malleate()

				err = lightClientModule.VerifyClientMessage(suite.chainA.GetContext(), clientID, clientMsg)

				if tc.expErr == nil {
					suite.Require().NoError(err)
				} else {
					suite.Require().ErrorContains(err, tc.expErr.Error())
				}
			})
		}
	}
}

func (suite *SoloMachineTestSuite) TestVerifyUpgradeAndUpdateState() {
	clientID := suite.solomachine.ClientID

	lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID)
	suite.Require().NoError(err)

	err = lightClientModule.VerifyUpgradeAndUpdateState(suite.chainA.GetContext(), clientID, nil, nil, nil, nil)
	suite.Require().Error(err)
}

func (suite *SoloMachineTestSuite) TestLatestHeight() {
	var clientID string

	testCases := []struct {
		name      string
		malleate  func()
		expHeight clienttypes.Height
	}{
		{
			"success",
			func() {},
			// Default as returned by solomachine.ClientState()
			clienttypes.NewHeight(0, 1),
		},
		{
			"failure: cannot find client state",
			func() {
				clientID = unusedSmClientID
			},
			clienttypes.ZeroHeight(),
		},
	}

	for _, tc := range testCases {
		suite.Run(tc.name, func() {
			suite.SetupTest()
			clientID = suite.solomachine.ClientID
			clientState := suite.solomachine.ClientState()

			lightClientModule, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(suite.chainA.GetContext(), clientID)
			suite.Require().NoError(err)

			suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), clientID, clientState)

			tc.malleate()

			height := lightClientModule.LatestHeight(suite.chainA.GetContext(), clientID)

			suite.Require().Equal(tc.expHeight, height)
		})
	}
}
