package net.corda.finance.contracts.asset

import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.VaultService
import net.corda.core.node.services.queryBy
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.OpaqueBytes
import net.corda.finance.*
import net.corda.finance.contracts.utils.sumCash
import net.corda.finance.contracts.utils.sumCashBy
import net.corda.finance.contracts.utils.sumCashOrNull
import net.corda.finance.contracts.utils.sumCashOrZero
import net.corda.finance.workflows.asset.CashUtils
import net.corda.node.services.vault.NodeVaultService
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.testing.core.*
import net.corda.testing.dsl.EnforceVerifyOrFail
import net.corda.testing.dsl.TransactionDSL
import net.corda.testing.dsl.TransactionDSLInterpreter
import net.corda.testing.internal.LogHelper
import net.corda.testing.internal.vault.VaultFiller
import net.corda.testing.node.MockServices
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
import net.corda.testing.node.ledger
import net.corda.testing.node.makeTestIdentityService
import net.corda.testing.node.transaction
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.util.*
import kotlin.test.*

class CashTests {
    private companion object {
        val alice = TestIdentity(ALICE_NAME, 70)
        val bob = TestIdentity(BOB_NAME, 80)
        val charlie = TestIdentity(CHARLIE_NAME, 90)
        val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10)
        val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
        val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
        val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
    }

    @Rule
    @JvmField
    val testSerialization = SerializationEnvironmentRule()
    private val defaultIssuer = megaCorp.ref(1)
    private val inState = Cash.State(
            amount = 1000.DOLLARS `issued by` defaultIssuer,
            owner = AnonymousParty(alice.publicKey)
    )
    // Input state held by the issuer
    private val issuerInState = inState.copy(owner = defaultIssuer.party)
    private val outState = issuerInState.copy(owner = AnonymousParty(bob.publicKey))

    private fun Cash.State.editDepositRef(ref: Byte) = copy(
            amount = Amount(amount.quantity, token = amount.token.copy(amount.token.issuer.copy(reference = OpaqueBytes.of(ref))))
    )

    private lateinit var ourServices: MockServices
    private lateinit var miniCorpServices: MockServices
    private lateinit var megaCorpServices: MockServices
    val vault: VaultService get() = miniCorpServices.vaultService
    lateinit var database: CordaPersistence
    private lateinit var vaultStatesUnconsumed: List<StateAndRef<Cash.State>>

    private lateinit var ourIdentity: AbstractParty
    private lateinit var miniCorpAnonymised: AnonymousParty
    private lateinit var cashStates: List<StateAndRef<Cash.State>>

    // TODO: Optimise this so that we don't throw away and rebuild state that can be shared across tests.
    @Before
    fun setUp() {
        val cordapps = listOf("net.corda.finance.contracts.asset", "net.corda.finance.schemas")
        LogHelper.setLevel(NodeVaultService::class)
        megaCorpServices = MockServices(cordapps, megaCorp)
        miniCorpServices = MockServices(cordapps, miniCorp)
        val myself = TestIdentity(CordaX500Name("Me", "London", "GB"))
        val databaseAndServices = makeTestDatabaseAndMockServices(
                cordapps,
                makeTestIdentityService(megaCorp.identity, miniCorp.identity, dummyCashIssuer.identity, dummyNotary.identity, myself.identity),
                myself
        )
        database = databaseAndServices.first
        ourServices = databaseAndServices.second

        // Set up and register identities
        ourIdentity = ourServices.myInfo.singleIdentity()
        miniCorpAnonymised = miniCorpServices.myInfo.singleIdentityAndCert().party.anonymise()

        // Create some cash. Any attempt to spend >$500 will require multiple issuers to be involved.
        database.transaction {
            val vaultFiller = VaultFiller(ourServices, dummyNotary)
            vaultFiller.fillWithSomeTestCash(100.DOLLARS, megaCorpServices, 1, megaCorp.ref(1), ourIdentity)
            vaultFiller.fillWithSomeTestCash(400.DOLLARS, megaCorpServices, 1, megaCorp.ref(1), ourIdentity)
            vaultFiller.fillWithSomeTestCash(80.DOLLARS, miniCorpServices, 1, miniCorp.ref(1), ourIdentity)
            vaultFiller.fillWithSomeTestCash(80.SWISS_FRANCS, miniCorpServices, 1, miniCorp.ref(1), ourIdentity)

            vaultFiller.fillWithSomeTestCash(100.POUNDS, megaCorpServices, 1, megaCorp.ref(1), ourIdentity)
            vaultFiller.fillWithSomeTestCash(400.POUNDS, megaCorpServices, 1, megaCorp.ref(1), ourIdentity)
            vaultFiller.fillWithSomeTestCash(80.POUNDS, miniCorpServices, 1, miniCorp.ref(1), ourIdentity)
        }
        database.transaction {
            vaultStatesUnconsumed = ourServices.vaultService.queryBy<Cash.State>().states
        }
        cashStates = listOf(
            makeCash(100.DOLLARS, megaCorp.party),
            makeCash(400.DOLLARS, megaCorp.party),
            makeCash(80.DOLLARS, miniCorp.party),
            makeCash(80.SWISS_FRANCS, miniCorp.party, 2)
        )
    }

    @After
    fun tearDown() {
        database.close()
    }

    private fun transaction(script: TransactionDSL<TransactionDSLInterpreter>.() -> EnforceVerifyOrFail) {
        MockServices(megaCorp).transaction(dummyNotary.party, script)
    }

    @Test(timeout=300_000)
	fun trivial() {
        transaction {
            attachment(Cash.PROGRAM_ID)
            input(Cash.PROGRAM_ID, inState)
            tweak {
                output(Cash.PROGRAM_ID, outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer))
                command(alice.publicKey, Cash.Commands.Move())
                this `fails with` "the amounts balance"
            }
            tweak {
                output(Cash.PROGRAM_ID, outState)
                command(alice.publicKey, DummyCommandData)
                // Invalid command
                this `fails with` "required net.corda.finance.contracts.asset.Cash.Commands.Move command"
            }
            tweak {
                output(Cash.PROGRAM_ID, outState)
                command(bob.publicKey, Cash.Commands.Move())
                this `fails with` "the owning keys are a subset of the signing keys"
            }
            tweak {
                output(Cash.PROGRAM_ID, outState)
                output(Cash.PROGRAM_ID, outState issuedBy miniCorp.party)
                command(alice.publicKey, Cash.Commands.Move())
                this `fails with` "at least one cash input"
            }
            // Simple reallocation works.
            tweak {
                output(Cash.PROGRAM_ID, outState)
                command(alice.publicKey, Cash.Commands.Move())
                this.verifies()
            }
        }
    }

    @Test(timeout=300_000)
	fun twoMoves() {
        transaction {
            attachment(Cash.PROGRAM_ID)
            input(Cash.PROGRAM_ID, inState)
            input(Cash.PROGRAM_ID, inState.copy(owner = bob.party))
            output(Cash.PROGRAM_ID, outState)
            command(alice.publicKey, Cash.Commands.Move())
            tweak {
                output(Cash.PROGRAM_ID, outState)
                command(bob.publicKey, Cash.Commands.Move())
                this.verifies()
            }
        }
    }

    @BelongsToContract(Cash::class)
    object DummyState: ContractState {
        override val participants: List<AbstractParty> = emptyList()
    }

    @Test(timeout=300_000)
	fun `issue by move`() {
        // Check we can't "move" money into existence.
        transaction {
            attachment(Cash.PROGRAM_ID)
            input(Cash.PROGRAM_ID, DummyState)
            output(Cash.PROGRAM_ID, outState)
            command(miniCorp.publicKey, Cash.Commands.Move())
            this `fails with` "there is at least one cash input for this group"
        }
    }

    @Test(timeout=300_000)
	fun issue() {
        // Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised
        // institution is allowed to issue as much cash as they want.
        transaction {
            attachment(Cash.PROGRAM_ID)
            output(Cash.PROGRAM_ID, outState)
            command(alice.publicKey, Cash.Commands.Issue())
            this `fails with` "output states are issued by a command signer"
        }
        transaction {
            attachment(Cash.PROGRAM_ID)
            output(Cash.PROGRAM_ID,
                Cash.State(
                        amount = 1000.DOLLARS `issued by` miniCorp.ref(12, 34),
                        owner = AnonymousParty(alice.publicKey)))
            command(miniCorp.publicKey, Cash.Commands.Issue())
            this.verifies()
        }
    }

    @Test(timeout=300_000)
	fun generateIssueRaw() {
        // Test generation works.
        val tx: WireTransaction = TransactionBuilder(notary = null).apply {
            Cash().generateIssue(this, 100.DOLLARS `issued by` miniCorp.ref(12, 34), owner = AnonymousParty(alice.publicKey), notary = dummyNotary.party)
        }.toWireTransaction(miniCorpServices)
        assertTrue(tx.inputs.isEmpty())
        val s = tx.outputsOfType<Cash.State>().single()
        assertEquals(100.DOLLARS `issued by` miniCorp.ref(12, 34), s.amount)
        assertEquals(miniCorp.party as AbstractParty, s.amount.token.issuer.party)
        assertEquals(AnonymousParty(alice.publicKey), s.owner)
        assertTrue(tx.commands[0].value is Cash.Commands.Issue)
        assertEquals(miniCorp.publicKey, tx.commands[0].signers[0])
    }

    @Test(timeout=300_000)
	fun generateIssueFromAmount() {
        // Test issuance from an issued amount
        val amount = 100.DOLLARS `issued by` miniCorp.ref(12, 34)
        val tx: WireTransaction = TransactionBuilder(notary = null).apply {
            Cash().generateIssue(this, amount, owner = AnonymousParty(alice.publicKey), notary = dummyNotary.party)
        }.toWireTransaction(miniCorpServices)
        assertTrue(tx.inputs.isEmpty())
        assertEquals(tx.outputs[0], tx.outputs[0])
    }

    @Test(timeout=300_000)
	fun `extended issue examples`() {
        // We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
        transaction {
            attachment(Cash.PROGRAM_ID)
            input(Cash.PROGRAM_ID, issuerInState)
            output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount * 2))
            // Move fails: not allowed to summon money.
            tweak {
                command(alice.publicKey, Cash.Commands.Move())
                this `fails with` "the amounts balance"
            }

            // Issue works.
            tweak {
                command(megaCorp.publicKey, Cash.Commands.Issue())
                this.verifies()
            }
        }

        // Can't use an issue command to lower the amount.
        transaction {
            attachment(Cash.PROGRAM_ID)
            input(Cash.PROGRAM_ID, inState)
            output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount.splitEvenly(2).first()))
            command(megaCorp.publicKey, Cash.Commands.Issue())
            this `fails with` "output values sum to more than the inputs"
        }

        // Can't have an issue command that doesn't actually issue money.
        transaction {
            attachment(Cash.PROGRAM_ID)
            input(Cash.PROGRAM_ID, inState)
            output(Cash.PROGRAM_ID, inState)
            command(megaCorp.publicKey, Cash.Commands.Issue())
            this `fails with` "output values sum to more than the inputs"
        }

        // Can't have any other commands if we have an issue command (because the issue command overrules them)
        transaction {
            attachment(Cash.PROGRAM_ID)
            input(Cash.PROGRAM_ID, inState)
            output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount * 2))
            command(megaCorp.publicKey, Cash.Commands.Issue())
            tweak {
                command(megaCorp.publicKey, Cash.Commands.Issue())
                this `fails with` "there is only a single issue command"
            }
            this.verifies()
        }
    }

    /**
     * Test that the issuance builder rejects building into a transaction with existing
     * cash inputs.
     */
    @Test(expected = IllegalStateException::class, timeout=300_000)
    fun `reject issuance with inputs`() {
        // Issue some cash
        var ptx = TransactionBuilder(dummyNotary.party)

        Cash().generateIssue(ptx, 100.DOLLARS `issued by` miniCorp.ref(12, 34), owner = miniCorp.party, notary = dummyNotary.party)
        val tx = miniCorpServices.signInitialTransaction(ptx)

        // Include the previously issued cash in a new issuance command
        ptx = TransactionBuilder(dummyNotary.party)
        ptx.addInputState(tx.tx.outRef<Cash.State>(0))
        Cash().generateIssue(ptx, 100.DOLLARS `issued by` miniCorp.ref(12, 34), owner = miniCorp.party, notary = dummyNotary.party)
    }

    @Test(timeout=300_000)
	fun testMergeSplit() {
        // Splitting value works.
        transaction {
            attachment(Cash.PROGRAM_ID)
            command(alice.publicKey, Cash.Commands.Move())
            tweak {
                input(Cash.PROGRAM_ID, inState)
                val splits4 = inState.amount.splitEvenly(4)
                for (i in 0..3) output(Cash.PROGRAM_ID, inState.copy(amount = splits4[i]))
                this.verifies()
            }
            // Merging 4 inputs into 2 outputs works.
            tweak {
                val splits2 = inState.amount.splitEvenly(2)
                val splits4 = inState.amount.splitEvenly(4)
                for (i in 0..3) input(Cash.PROGRAM_ID, inState.copy(amount = splits4[i]))
                for (i in 0..1) output(Cash.PROGRAM_ID, inState.copy(amount = splits2[i]))
                this.verifies()
            }
            // Merging 2 inputs into 1 works.
            tweak {
                val splits2 = inState.amount.splitEvenly(2)
                for (i in 0..1) input(Cash.PROGRAM_ID, inState.copy(amount = splits2[i]))
                output(Cash.PROGRAM_ID, inState)
                this.verifies()
            }
        }
    }

    @Test(timeout=300_000)
	fun zeroSizedValues() {
        transaction {
            attachment(Cash.PROGRAM_ID)
            input(Cash.PROGRAM_ID, inState)
            input(Cash.PROGRAM_ID, inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer))
            command(alice.publicKey, Cash.Commands.Move())
            this `fails with` "zero sized inputs"
        }
        transaction {
            attachment(Cash.PROGRAM_ID)
            input(Cash.PROGRAM_ID, inState)
            output(Cash.PROGRAM_ID, inState)
            output(Cash.PROGRAM_ID, inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer))
            command(alice.publicKey, Cash.Commands.Move())
            this `fails with` "zero sized outputs"
        }
    }

    @Test(timeout=300_000)
	fun trivialMismatches() {
        // Can't change issuer.
        transaction {
            attachment(Cash.PROGRAM_ID)
            input(Cash.PROGRAM_ID, inState)
            output(Cash.PROGRAM_ID, outState issuedBy miniCorp.party)
            command(alice.publicKey, Cash.Commands.Move())
            this `fails with` "the amounts balance"
        }
        // Can't change deposit reference when splitting.
        transaction {
            attachment(Cash.PROGRAM_ID)
            val splits2 = inState.amount.splitEvenly(2)
            input(Cash.PROGRAM_ID, inState)
            for (i in 0..1) output(Cash.PROGRAM_ID, outState.copy(amount = splits2[i]).editDepositRef(i.toByte()))
            command(alice.publicKey, Cash.Commands.Move())
            this `fails with` "the amounts balance"
        }
        // Can't mix currencies.
        transaction {
            attachment(Cash.PROGRAM_ID)
            input(Cash.PROGRAM_ID, inState)
            output(Cash.PROGRAM_ID, outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer))
            output(Cash.PROGRAM_ID, outState.copy(amount = 200.POUNDS `issued by` defaultIssuer))
            command(alice.publicKey, Cash.Commands.Move())
            this `fails with` "the amounts balance"
        }
        transaction {
            attachment(Cash.PROGRAM_ID)
            input(Cash.PROGRAM_ID, inState)
            input(Cash.PROGRAM_ID,
                inState.copy(
                        amount = 150.POUNDS `issued by` defaultIssuer,
                        owner = AnonymousParty(bob.publicKey)))
            output(Cash.PROGRAM_ID, outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer))
            command(alice.publicKey, Cash.Commands.Move())
            this `fails with` "the amounts balance"
        }
        // Can't have superfluous input states from different issuers.
        transaction {
            attachment(Cash.PROGRAM_ID)
            input(Cash.PROGRAM_ID, inState)
            input(Cash.PROGRAM_ID, inState issuedBy miniCorp.party)
            output(Cash.PROGRAM_ID, outState)
            command(alice.publicKey, Cash.Commands.Move())
            this `fails with` "the amounts balance"
        }
        // Can't combine two different deposits at the same issuer.
        transaction {
            attachment(Cash.PROGRAM_ID)
            input(Cash.PROGRAM_ID, inState)
            input(Cash.PROGRAM_ID, inState.editDepositRef(3))
            output(Cash.PROGRAM_ID, outState.copy(amount = inState.amount * 2).editDepositRef(3))
            command(alice.publicKey, Cash.Commands.Move())
            this `fails with` "for reference [01]"
        }
    }

    @Test(timeout=300_000)
	fun exitLedger() {
        // Single input/output straightforward case.
        transaction {
            attachment(Cash.PROGRAM_ID)
            input(Cash.PROGRAM_ID, issuerInState)
            output(Cash.PROGRAM_ID, issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)))
            tweak {
                command(megaCorp.publicKey, Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer))
                command(megaCorp.publicKey, Cash.Commands.Move())
                this `fails with` "the amounts balance"
            }

            tweak {
                command(megaCorp.publicKey, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer))
                this `fails with` "required net.corda.finance.contracts.asset.Cash.Commands.Move command"

                tweak {
                    command(megaCorp.publicKey, Cash.Commands.Move())
                    this.verifies()
                }
            }
        }
    }

    @Test(timeout=300_000)
	fun `exit ledger with multiple issuers`() {
        // Multi-issuer case.
        transaction {
            attachment(Cash.PROGRAM_ID)
            input(Cash.PROGRAM_ID, issuerInState)
            input(Cash.PROGRAM_ID, issuerInState.copy(owner = miniCorp.party) issuedBy miniCorp.party)
            output(Cash.PROGRAM_ID, issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) issuedBy miniCorp.party)
            output(Cash.PROGRAM_ID, issuerInState.copy(owner = miniCorp.party, amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)))
            command(listOf(megaCorp.publicKey, miniCorp.publicKey), Cash.Commands.Move())
            this `fails with` "the amounts balance"
            command(megaCorp.publicKey, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer))
            this `fails with` "the amounts balance"
            command(miniCorp.publicKey, Cash.Commands.Exit(200.DOLLARS `issued by` miniCorp.ref(1)))
            this.verifies()
        }
    }

    @Test(timeout=300_000)
	fun `exit cash not held by its issuer`() {
        // Single input/output straightforward case.
        transaction {
            attachment(Cash.PROGRAM_ID)
            input(Cash.PROGRAM_ID, inState)
            output(Cash.PROGRAM_ID, outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)))
            command(megaCorp.publicKey, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer))
            command(alice.publicKey, Cash.Commands.Move())
            this `fails with` "the amounts balance"
        }
    }

    @Test(timeout=300_000)
	fun multiIssuer() {
        transaction {
            attachment(Cash.PROGRAM_ID)
            // Gather 2000 dollars from two different issuers.
            input(Cash.PROGRAM_ID, inState)
            input(Cash.PROGRAM_ID, inState issuedBy miniCorp.party)
            command(alice.publicKey, Cash.Commands.Move())
            // Can't merge them together.
            tweak {
                output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(bob.publicKey), amount = 2000.DOLLARS `issued by` defaultIssuer))
                this `fails with` "the amounts balance"
            }
            // Missing MiniCorp deposit
            tweak {
                output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(bob.publicKey)))
                output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(bob.publicKey)))
                this `fails with` "the amounts balance"
            }

            // This works.
            output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(bob.publicKey)))
            output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(bob.publicKey)) issuedBy miniCorp.party)
            this.verifies()
        }
    }

    @Test(timeout=300_000)
	fun multiCurrency() {
        // Check we can do an atomic currency trade tx.
        transaction {
            attachment(Cash.PROGRAM_ID)
            val pounds = Cash.State(658.POUNDS `issued by` miniCorp.ref(3, 4, 5), AnonymousParty(bob.publicKey))
            input(Cash.PROGRAM_ID, inState ownedBy AnonymousParty(alice.publicKey))
            input(Cash.PROGRAM_ID, pounds)
            output(Cash.PROGRAM_ID, inState ownedBy AnonymousParty(bob.publicKey))
            output(Cash.PROGRAM_ID, pounds ownedBy AnonymousParty(alice.publicKey))
            command(listOf(alice.publicKey, bob.publicKey), Cash.Commands.Move())
            this.verifies()
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    //
    // Spend tx generation

    private fun makeCash(amount: Amount<Currency>, issuer: AbstractParty, depositRef: Byte = 1) =
            StateAndRef(
                    TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), ourIdentity), Cash.PROGRAM_ID, dummyNotary.party, constraint = AlwaysAcceptAttachmentConstraint),
                    StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
            )

    /**
     * Generate an exit transaction, removing some amount of cash from the ledger.
     */
    private fun makeExit(serviceHub: ServiceHub, amount: Amount<Currency>, issuer: Party, depositRef: Byte = 1): WireTransaction {
        val tx = TransactionBuilder(dummyNotary.party)
        val payChangeTo = serviceHub.keyManagementService.freshKeyAndCert(miniCorp.identity, false).party
        Cash().generateExit(tx, Amount(amount.quantity, Issued(issuer.ref(depositRef), amount.token)), cashStates, payChangeTo)
        return tx.toWireTransaction(serviceHub)
    }

    private fun makeSpend(services: ServiceHub, amount: Amount<Currency>, dest: AbstractParty): WireTransaction {
        val ourIdentity = services.myInfo.singleIdentityAndCert()
        val tx = TransactionBuilder(dummyNotary.party)
        database.transaction {
            CashUtils.generateSpend(services, tx, amount, ourIdentity, dest)
        }
        return tx.toWireTransaction(services)
    }

    /**
     * Try exiting an amount which matches a single state.
     */
    @Test(timeout=300_000)
	fun generateSimpleExit() {
        val wtx = makeExit(miniCorpServices, 100.DOLLARS, megaCorp.party, 1)
        assertEquals(cashStates[0].ref, wtx.inputs[0])
        assertEquals(0, wtx.outputs.size)

        val expectedMove = Cash.Commands.Move()
        val expectedExit = Cash.Commands.Exit(Amount(10000, Issued(megaCorp.ref(1), USD)))

        assertEquals(listOf(expectedMove, expectedExit), wtx.commands.map { it.value })
    }

    /**
     * Try exiting an amount smaller than the smallest available input state, and confirm change is generated correctly.
     */
    @Test(timeout=300_000)
	fun generatePartialExit() {
        val wtx = makeExit(miniCorpServices, 50.DOLLARS, megaCorp.party, 1)
        val actualInput = wtx.inputs.single()
        // Filter the available inputs and confirm exactly one has been used
        val expectedInputs = cashStates.filter { it.ref == actualInput }
        assertEquals(1, expectedInputs.size)
        val inputState = expectedInputs.single()
        val actualChange = wtx.outputs.single().data as Cash.State
        val expectedChangeAmount = inputState.state.data.amount.quantity - 50.DOLLARS.quantity
        val expectedChange = cashStates[0].state.data.copy(amount = cashStates[0].state.data.amount.copy(quantity = expectedChangeAmount), owner = actualChange.owner)
        assertEquals(expectedChange, wtx.getOutput(0))
    }

    /**
     * Try exiting a currency we don't have.
     */
    @Test(timeout=300_000)
	fun generateAbsentExit() {
        assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, megaCorp.party, 1) }
    }

    /**
     * Try exiting with a reference mis-match.
     */
    @Test(timeout=300_000)
	fun generateInvalidReferenceExit() {
        assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, megaCorp.party, 2) }
    }

    /**
     * Try exiting an amount greater than the maximum available.
     */
    @Test(timeout=300_000)
	fun generateInsufficientExit() {
        assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 1000.DOLLARS, megaCorp.party, 1) }
    }

    /**
     * Try exiting for an owner with no states
     */
    @Test(timeout=300_000)
	fun generateOwnerWithNoStatesExit() {
        assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, charlie.party, 1) }
    }

    /**
     * Try exiting when vault is empty
     */
    @Test(timeout=300_000)
	fun generateExitWithEmptyVault() {
        assertFailsWith<IllegalArgumentException> {
            val tx = TransactionBuilder(dummyNotary.party)
            Cash().generateExit(tx, Amount(100, Issued(charlie.ref(1), GBP)), emptyList(), ourIdentity)
        }
    }

    @Test(timeout=300_000)
	fun generateSimpleDirectSpend() {
        val wtx =
                database.transaction {
                    makeSpend(ourServices, 100.DOLLARS, miniCorpAnonymised)
                }
        database.transaction {
            val vaultState = vaultStatesUnconsumed.elementAt(0)
            assertEquals(vaultState.ref, wtx.inputs[0])
            assertEquals(vaultState.state.data.copy(owner = miniCorpAnonymised), wtx.getOutput(0))
            assertEquals(ourIdentity.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
        }
    }

    @Test(timeout=300_000)
	fun generateSimpleSpendWithParties() {
        database.transaction {
            val tx = TransactionBuilder(dummyNotary.party)
            CashUtils.generateSpend(ourServices, tx, 80.DOLLARS, ourServices.myInfo.singleIdentityAndCert(), alice.party, setOf(miniCorp.party))

            assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0])
        }
    }

    @Test(timeout=300_000)
	fun generateSimpleSpendWithChange() {
        val wtx =
                database.transaction {
                    makeSpend(ourServices, 10.DOLLARS, miniCorpAnonymised)
                }
        database.transaction {
            val vaultState = vaultStatesUnconsumed.elementAt(0)
            val changeAmount = 90.DOLLARS `issued by` defaultIssuer
            val likelyChangeState = wtx.outputs.map(TransactionState<*>::data).single { state ->
                if (state is Cash.State) {
                    state.amount == changeAmount
                } else {
                    false
                }
            }
            val changeOwner = (likelyChangeState as Cash.State).owner
            assertEquals(1, ourServices.keyManagementService.filterMyKeys(setOf(changeOwner.owningKey)).toList().size)
            assertEquals(vaultState.ref, wtx.inputs[0])
            assertEquals(vaultState.state.data.copy(owner = miniCorpAnonymised, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data)
            assertEquals(vaultState.state.data.copy(amount = changeAmount, owner = changeOwner), wtx.outputs[1].data)
            assertEquals(ourIdentity.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
        }
    }

    @Test(timeout=300_000)
	fun generateSpendWithTwoInputs() {
        val wtx =
                database.transaction {
                    makeSpend(ourServices, 500.DOLLARS, miniCorpAnonymised)
                }
        database.transaction {
            val vaultState0 = vaultStatesUnconsumed.elementAt(0)
            val vaultState1 = vaultStatesUnconsumed.elementAt(1)
            assertEquals(vaultState0.ref, wtx.inputs[0])
            assertEquals(vaultState1.ref, wtx.inputs[1])
            assertEquals(vaultState0.state.data.copy(owner = miniCorpAnonymised, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.getOutput(0))
            assertEquals(ourIdentity.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
        }
    }

    @Test(timeout=300_000)
	fun generateSpendMixedDeposits() {
        val wtx =
                database.transaction {
                    val wtx = makeSpend(ourServices, 580.DOLLARS, miniCorpAnonymised)
                    assertEquals(3, wtx.inputs.size)
                    wtx
                }
        database.transaction {
            val vaultState0: StateAndRef<Cash.State> = vaultStatesUnconsumed.elementAt(0)
            val vaultState1: StateAndRef<Cash.State> = vaultStatesUnconsumed.elementAt(1)
            val vaultState2: StateAndRef<Cash.State> = vaultStatesUnconsumed.elementAt(2)
            assertEquals(vaultState0.ref, wtx.inputs[0])
            assertEquals(vaultState1.ref, wtx.inputs[1])
            assertEquals(vaultState2.ref, wtx.inputs[2])
            assertEquals(vaultState0.state.data.copy(owner = miniCorpAnonymised, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data)
            assertEquals(vaultState2.state.data.copy(owner = miniCorpAnonymised), wtx.outputs[0].data)
            assertEquals(ourIdentity.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
        }
    }

    @Test(timeout=300_000)
	fun generateSpendInsufficientBalance() {
        database.transaction {
            val e = assertFailsWith<InsufficientBalanceException> {
                makeSpend(ourServices, 1000.DOLLARS, miniCorpAnonymised)
            }
            assertEquals((1000 - 580).DOLLARS, e.amountMissing)

            assertFailsWith(InsufficientBalanceException::class) {
                makeSpend(ourServices, 81.SWISS_FRANCS, miniCorpAnonymised)
            }
        }
    }

    /**
     * Confirm that aggregation of states is correctly modelled.
     */
    @Test(timeout=300_000)
	fun aggregation() {
        val fiveThousandDollarsFromMega = Cash.State(5000.DOLLARS `issued by` megaCorp.ref(2), megaCorp.party)
        val twoThousandDollarsFromMega = Cash.State(2000.DOLLARS `issued by` megaCorp.ref(2), miniCorp.party)
        val oneThousandDollarsFromMini = Cash.State(1000.DOLLARS `issued by` miniCorp.ref(3), megaCorp.party)

        // Obviously it must be possible to aggregate states with themselves
        assertEquals(fiveThousandDollarsFromMega.amount.token, fiveThousandDollarsFromMega.amount.token)

        // Owner is not considered when calculating whether it is possible to aggregate states
        assertEquals(fiveThousandDollarsFromMega.amount.token, twoThousandDollarsFromMega.amount.token)

        // States cannot be aggregated if the deposit differs
        assertNotEquals(fiveThousandDollarsFromMega.amount.token, oneThousandDollarsFromMini.amount.token)
        assertNotEquals(twoThousandDollarsFromMega.amount.token, oneThousandDollarsFromMini.amount.token)

        // States cannot be aggregated if the currency differs
        assertNotEquals(oneThousandDollarsFromMini.amount.token,
                Cash.State(1000.POUNDS `issued by` miniCorp.ref(3), megaCorp.party).amount.token)

        // States cannot be aggregated if the reference differs
        assertNotEquals(fiveThousandDollarsFromMega.amount.token, (fiveThousandDollarsFromMega withDeposit defaultIssuer).amount.token)
        assertNotEquals((fiveThousandDollarsFromMega withDeposit defaultIssuer).amount.token, fiveThousandDollarsFromMega.amount.token)
    }

    @Test(timeout=300_000)
	fun `summing by owner`() {
        val states = listOf(
                Cash.State(1000.DOLLARS `issued by` defaultIssuer, miniCorp.party),
                Cash.State(2000.DOLLARS `issued by` defaultIssuer, megaCorp.party),
                Cash.State(4000.DOLLARS `issued by` defaultIssuer, megaCorp.party)
        )
        assertEquals(6000.DOLLARS `issued by` defaultIssuer, states.sumCashBy(megaCorp.party))
    }

    @Test(expected = UnsupportedOperationException::class, timeout=300_000)
    fun `summing by owner throws`() {
        val states = listOf(
                Cash.State(2000.DOLLARS `issued by` defaultIssuer, megaCorp.party),
                Cash.State(4000.DOLLARS `issued by` defaultIssuer, megaCorp.party)
        )
        states.sumCashBy(miniCorp.party)
    }

    @Test(timeout=300_000)
	fun `summing no currencies`() {
        val states = emptyList<Cash.State>()
        assertEquals(0.POUNDS `issued by` defaultIssuer, states.sumCashOrZero(GBP `issued by` defaultIssuer))
        assertNull(states.sumCashOrNull())
    }

    @Test(expected = UnsupportedOperationException::class, timeout=300_000)
    fun `summing no currencies throws`() {
        val states = emptyList<Cash.State>()
        states.sumCash()
    }

    @Test(timeout=300_000)
	fun `summing a single currency`() {
        val states = listOf(
                Cash.State(1000.DOLLARS `issued by` defaultIssuer, megaCorp.party),
                Cash.State(2000.DOLLARS `issued by` defaultIssuer, megaCorp.party),
                Cash.State(4000.DOLLARS `issued by` defaultIssuer, megaCorp.party)
        )
        // Test that summing everything produces the total number of dollars
        val expected = 7000.DOLLARS `issued by` defaultIssuer
        val actual = states.sumCash()
        assertEquals(expected, actual)
    }

    @Test(expected = IllegalArgumentException::class, timeout=300_000)
    fun `summing multiple currencies`() {
        val states = listOf(
                Cash.State(1000.DOLLARS `issued by` defaultIssuer, megaCorp.party),
                Cash.State(4000.POUNDS `issued by` defaultIssuer, megaCorp.party)
        )
        // Test that summing everything fails because we're mixing units
        states.sumCash()
    }

    // Double spend.
    @Test(timeout=300_000)
	fun chainCashDoubleSpendFailsWith() {
        MockServices(megaCorp).ledger(dummyNotary.party) {
            unverifiedTransaction {
                attachment(Cash.PROGRAM_ID)
                output(Cash.PROGRAM_ID, "MEGA_CORP cash",
                    Cash.State(
                            amount = 1000.DOLLARS `issued by` megaCorp.ref(1, 1),
                            owner = megaCorp.party))
            }

            transaction {
                attachment(Cash.PROGRAM_ID)
                input("MEGA_CORP cash")
                output(Cash.PROGRAM_ID, "MEGA_CORP cash 2", "MEGA_CORP cash".output<Cash.State>().copy(owner = AnonymousParty(alice.publicKey)))
                command(megaCorp.publicKey, Cash.Commands.Move())
                this.verifies()
            }

            tweak {
                transaction {
                    attachment(Cash.PROGRAM_ID)
                    input("MEGA_CORP cash")
                    // We send it to another pubkey so that the transaction is not identical to the previous one
                    output(Cash.PROGRAM_ID, "MEGA_CORP cash 3", "MEGA_CORP cash".output<Cash.State>().copy(owner = alice.party))
                    command(megaCorp.publicKey, Cash.Commands.Move())
                    this.verifies()
                }
                this.fails()
            }

            this.verifies()
        }
    }

    @Test(timeout=300_000)
	fun multiSpend() {
        val tx = TransactionBuilder(dummyNotary.party)
        database.transaction {
            val payments = listOf(
                    PartyAndAmount(miniCorpAnonymised, 400.DOLLARS),
                    PartyAndAmount(charlie.party.anonymise(), 150.DOLLARS)
            )
            CashUtils.generateSpend(ourServices, tx, payments, ourServices.myInfo.singleIdentityAndCert())
        }
        val wtx = tx.toWireTransaction(ourServices)
        fun out(i: Int) = wtx.getOutput(i) as Cash.State
        assertEquals(4, wtx.outputs.size)
        assertEquals(80.DOLLARS, out(0).amount.withoutIssuer())
        assertEquals(320.DOLLARS, out(1).amount.withoutIssuer())
        assertEquals(150.DOLLARS, out(2).amount.withoutIssuer())
        assertEquals(30.DOLLARS, out(3).amount.withoutIssuer())
        assertEquals(miniCorp.party, out(0).amount.token.issuer.party)
        assertEquals(megaCorp.party, out(1).amount.token.issuer.party)
        assertEquals(megaCorp.party, out(2).amount.token.issuer.party)
        assertEquals(megaCorp.party, out(3).amount.token.issuer.party)
    }

    @Test(timeout=300_000)
	fun generateSpendTwiceWithinATransaction() {
        val tx = TransactionBuilder(dummyNotary.party)
        database.transaction {
            val payments = listOf(
                    PartyAndAmount(miniCorpAnonymised, 400.DOLLARS),
                    PartyAndAmount(charlie.party.anonymise(), 150.DOLLARS)
            )
            CashUtils.generateSpend(ourServices, tx, payments, ourServices.myInfo.singleIdentityAndCert())
        }
        database.transaction {
            val payments = listOf(
                    PartyAndAmount(miniCorpAnonymised, 400.POUNDS),
                    PartyAndAmount(charlie.party.anonymise(), 150.POUNDS)
            )
            CashUtils.generateSpend(ourServices, tx, payments, ourServices.myInfo.singleIdentityAndCert())
        }

        val wtx = tx.toWireTransaction(ourServices)
        fun out(i: Int) = wtx.getOutput(i) as Cash.State
        assertEquals(8, wtx.outputs.size)

        assertEquals(80.DOLLARS, out(0).amount.withoutIssuer())
        assertEquals(320.DOLLARS, out(1).amount.withoutIssuer())
        assertEquals(150.DOLLARS, out(2).amount.withoutIssuer())
        assertEquals(30.DOLLARS, out(3).amount.withoutIssuer())
        assertEquals(miniCorp.party, out(0).amount.token.issuer.party)
        assertEquals(megaCorp.party, out(1).amount.token.issuer.party)
        assertEquals(megaCorp.party, out(2).amount.token.issuer.party)
        assertEquals(megaCorp.party, out(3).amount.token.issuer.party)

        assertEquals(80.POUNDS, out(4).amount.withoutIssuer())
        assertEquals(320.POUNDS, out(5).amount.withoutIssuer())
        assertEquals(150.POUNDS, out(6).amount.withoutIssuer())
        assertEquals(30.POUNDS, out(7).amount.withoutIssuer())
        assertEquals(miniCorp.party, out(4).amount.token.issuer.party)
        assertEquals(megaCorp.party, out(5).amount.token.issuer.party)
        assertEquals(megaCorp.party, out(6).amount.token.issuer.party)
        assertEquals(megaCorp.party, out(7).amount.token.issuer.party)

        assertEquals(2, wtx.commands.size)
    }
}
