package net.corda.irs.contract

import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import net.corda.core.contracts.Amount
import net.corda.core.contracts.UniqueIdentifier
import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.node.services.IdentityService
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.seconds
import net.corda.finance.DOLLARS
import net.corda.finance.EUR
import net.corda.finance.contracts.*
import net.corda.finance.workflows.utils.loadTestCalendar
import net.corda.testing.common.internal.addNotary
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.dsl.*
import net.corda.testing.node.MockServices
import net.corda.testing.node.ledger
import net.corda.testing.node.transaction
import org.junit.Rule
import org.junit.Test
import java.math.BigDecimal
import java.time.Instant
import java.time.LocalDate
import java.util.*
import kotlin.test.assertEquals

private val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z")
private val DUMMY_PARTY = Party(CordaX500Name("Dummy", "Madrid", "ES"), generateKeyPair().public)
private val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
private val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
private val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
private val ORACLE_PUBKEY = TestIdentity(CordaX500Name("Oracle", "London", "GB")).publicKey
private val DUMMY_NOTARY get() = dummyNotary.party
private val MEGA_CORP get() = megaCorp.party
private val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
private val MINI_CORP get() = miniCorp.party
fun createDummyIRS(irsSelect: Int): InterestRateSwap.State {
    return when (irsSelect) {
        1 -> {

            val fixedLeg = InterestRateSwap.FixedLeg(
                    fixedRatePayer = MEGA_CORP,
                    notional = 15900000.DOLLARS,
                    paymentFrequency = Frequency.SemiAnnual,
                    effectiveDate = LocalDate.of(2016, 3, 10),
                    effectiveDateAdjustment = null,
                    terminationDate = LocalDate.of(2026, 3, 10),
                    terminationDateAdjustment = null,
                    fixedRate = FixedRate(PercentageRatioUnit("1.677")),
                    dayCountBasisDay = DayCountBasisDay.D30,
                    dayCountBasisYear = DayCountBasisYear.Y360,
                    rollConvention = DateRollConvention.ModifiedFollowing,
                    dayInMonth = 10,
                    paymentRule = PaymentRule.InArrears,
                    paymentDelay = 3,
                    paymentCalendar = loadTestCalendar("London") + loadTestCalendar("NewYork"),
                    interestPeriodAdjustment = AccrualAdjustment.Adjusted
            )

            val floatingLeg = InterestRateSwap.FloatingLeg(
                    floatingRatePayer = MINI_CORP,
                    notional = 15900000.DOLLARS,
                    paymentFrequency = Frequency.Quarterly,
                    effectiveDate = LocalDate.of(2016, 3, 10),
                    effectiveDateAdjustment = null,
                    terminationDate = LocalDate.of(2026, 3, 10),
                    terminationDateAdjustment = null,
                    dayCountBasisDay = DayCountBasisDay.D30,
                    dayCountBasisYear = DayCountBasisYear.Y360,
                    rollConvention = DateRollConvention.ModifiedFollowing,
                    fixingRollConvention = DateRollConvention.ModifiedFollowing,
                    dayInMonth = 10,
                    resetDayInMonth = 10,
                    paymentRule = PaymentRule.InArrears,
                    paymentDelay = 3,
                    paymentCalendar = loadTestCalendar("London") + loadTestCalendar("NewYork"),
                    interestPeriodAdjustment = AccrualAdjustment.Adjusted,
                    fixingPeriodOffset = 2,
                    resetRule = PaymentRule.InAdvance,
                    fixingsPerPayment = Frequency.Quarterly,
                    fixingCalendar = loadTestCalendar("London"),
                    index = "LIBOR",
                    indexSource = "TEL3750",
                    indexTenor = Tenor("3M")
            )

            val calculation = InterestRateSwap.Calculation(

                    // TODO: this seems to fail quite dramatically
                    //expression = "fixedLeg.notional * fixedLeg.fixedRate",

                    // TODO: How I want it to look
                    //expression = "( fixedLeg.notional * (fixedLeg.fixedRate)) - (floatingLeg.notional * (rateSchedule.get(context.getDate('currentDate'))))",

                    // How it's ended up looking, which I think is now broken but it's a WIP.
                    expression = Expression("( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -" +
                            "(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))"),

                    floatingLegPaymentSchedule = mutableMapOf(),
                    fixedLegPaymentSchedule = mutableMapOf()
            )

            val common = InterestRateSwap.Common(
                    baseCurrency = EUR,
                    eligibleCurrency = EUR,
                    eligibleCreditSupport = "Cash in an Eligible Currency",
                    independentAmounts = Amount(0, EUR),
                    threshold = Amount(0, EUR),
                    minimumTransferAmount = Amount(250000 * 100, EUR),
                    rounding = Amount(10000 * 100, EUR),
                    valuationDateDescription = "Every Local Business Day",
                    notificationTime = "2:00pm London",
                    resolutionTime = "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
                    interestRate = ReferenceRate("T3270", Tenor("6M"), "EONIA"),
                    addressForTransfers = "",
                    exposure = UnknownType(),
                    localBusinessDay = loadTestCalendar("London"),
                    tradeID = "trade1",
                    hashLegalDocs = "put hash here",
                    dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360")
            )

            InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common, oracle = DUMMY_PARTY)
        }
        2 -> {
            // 10y swap, we pay 1.3% fixed 30/360 semi, rec 3m usd libor act/360 Q on 25m notional (mod foll/adj on both sides)
            // I did a mock up start date 10/03/2015 – 10/03/2025 so you have 5 cashflows on float side that have been preset the rest are unknown

            val fixedLeg = InterestRateSwap.FixedLeg(
                    fixedRatePayer = MEGA_CORP,
                    notional = 25000000.DOLLARS,
                    paymentFrequency = Frequency.SemiAnnual,
                    effectiveDate = LocalDate.of(2015, 3, 10),
                    effectiveDateAdjustment = null,
                    terminationDate = LocalDate.of(2025, 3, 10),
                    terminationDateAdjustment = null,
                    fixedRate = FixedRate(PercentageRatioUnit("1.3")),
                    dayCountBasisDay = DayCountBasisDay.D30,
                    dayCountBasisYear = DayCountBasisYear.Y360,
                    rollConvention = DateRollConvention.ModifiedFollowing,
                    dayInMonth = 10,
                    paymentRule = PaymentRule.InArrears,
                    paymentDelay = 0,
                    paymentCalendar = BusinessCalendar.EMPTY,
                    interestPeriodAdjustment = AccrualAdjustment.Adjusted
            )

            val floatingLeg = InterestRateSwap.FloatingLeg(
                    floatingRatePayer = MINI_CORP,
                    notional = 25000000.DOLLARS,
                    paymentFrequency = Frequency.Quarterly,
                    effectiveDate = LocalDate.of(2015, 3, 10),
                    effectiveDateAdjustment = null,
                    terminationDate = LocalDate.of(2025, 3, 10),
                    terminationDateAdjustment = null,
                    dayCountBasisDay = DayCountBasisDay.DActual,
                    dayCountBasisYear = DayCountBasisYear.Y360,
                    rollConvention = DateRollConvention.ModifiedFollowing,
                    fixingRollConvention = DateRollConvention.ModifiedFollowing,
                    dayInMonth = 10,
                    resetDayInMonth = 10,
                    paymentRule = PaymentRule.InArrears,
                    paymentDelay = 0,
                    paymentCalendar = BusinessCalendar.EMPTY,
                    interestPeriodAdjustment = AccrualAdjustment.Adjusted,
                    fixingPeriodOffset = 2,
                    resetRule = PaymentRule.InAdvance,
                    fixingsPerPayment = Frequency.Quarterly,
                    fixingCalendar = BusinessCalendar.EMPTY,
                    index = "USD LIBOR",
                    indexSource = "TEL3750",
                    indexTenor = Tenor("3M")
            )

            val calculation = InterestRateSwap.Calculation(

                    // TODO: this seems to fail quite dramatically
                    //expression = "fixedLeg.notional * fixedLeg.fixedRate",

                    // TODO: How I want it to look
                    //expression = "( fixedLeg.notional * (fixedLeg.fixedRate)) - (floatingLeg.notional * (rateSchedule.get(context.getDate('currentDate'))))",

                    // How it's ended up looking, which I think is now broken but it's a WIP.
                    expression = Expression("( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -" +
                            "(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))"),

                    floatingLegPaymentSchedule = mutableMapOf(),
                    fixedLegPaymentSchedule = mutableMapOf()
            )

            val common = InterestRateSwap.Common(
                    baseCurrency = EUR,
                    eligibleCurrency = EUR,
                    eligibleCreditSupport = "Cash in an Eligible Currency",
                    independentAmounts = Amount(0, EUR),
                    threshold = Amount(0, EUR),
                    minimumTransferAmount = Amount(250000 * 100, EUR),
                    rounding = Amount(10000 * 100, EUR),
                    valuationDateDescription = "Every Local Business Day",
                    notificationTime = "2:00pm London",
                    resolutionTime = "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
                    interestRate = ReferenceRate("T3270", Tenor("6M"), "EONIA"),
                    addressForTransfers = "",
                    exposure = UnknownType(),
                    localBusinessDay = loadTestCalendar("London"),
                    tradeID = "trade2",
                    hashLegalDocs = "put hash here",
                    dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360")
            )

            return InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common, oracle = DUMMY_PARTY)
        }
        else -> TODO("IRS number $irsSelect not defined")
    }
}

class IRSTests {
    @Rule
    @JvmField
    val testSerialization = SerializationEnvironmentRule()
    private val cordappPackages = listOf("net.corda.irs.contract")
    private val networkParameters = testNetworkParameters().addNotary(dummyNotary.party)
    private val megaCorpServices = MockServices(cordappPackages, megaCorp, mock(), networkParameters, megaCorp.keyPair)
    private val miniCorpServices = MockServices(cordappPackages, miniCorp, mock(), networkParameters,  miniCorp.keyPair)
    private val notaryServices = MockServices(cordappPackages, dummyNotary, mock(), networkParameters, dummyNotary.keyPair)
    private val ledgerServices = MockServices(
            emptyList(),
            megaCorp,
            mock<IdentityService>().also {
                doReturn(megaCorp.party).whenever(it).partyFromKey(megaCorp.publicKey)
                doReturn(null).whenever(it).partyFromKey(ORACLE_PUBKEY)
            },
            networkParameters
    )

    @Test(timeout=300_000)
	fun ok() {
        trade().verifies()
    }

    @Test(timeout=300_000)
	fun `ok with groups`() {
        tradegroups().verifies()
    }

    /**
     * Generate an IRS txn - we'll need it for a few things.
     */
    private fun generateIRSTxn(irsSelect: Int): SignedTransaction {
        val dummyIRS = createDummyIRS(irsSelect)
        return run {
            val gtx = InterestRateSwap().generateAgreement(
                    fixedLeg = dummyIRS.fixedLeg,
                    floatingLeg = dummyIRS.floatingLeg,
                    calculation = dummyIRS.calculation,
                    common = dummyIRS.common,
                    oracle = DUMMY_PARTY,
                    notary = DUMMY_NOTARY).apply {
                setTimeWindow(TEST_TX_TIME, 30.seconds)
            }
            val ptx1 = megaCorpServices.signInitialTransaction(gtx)
            val ptx2 = miniCorpServices.addSignature(ptx1)
            notaryServices.addSignature(ptx2)
        }
    }

    /**
     * Just make sure it's sane.
     */
    @Test(timeout=300_000)
	fun pprintIRS() {
        val irs = singleIRS()
        println(irs.prettyPrint())
    }

    /**
     * Utility so I don't have to keep typing this.
     */
    fun singleIRS(irsSelector: Int = 1): InterestRateSwap.State {
        return generateIRSTxn(irsSelector).tx.outputsOfType<InterestRateSwap.State>().single()
    }

    /**
     * Test the generate. No explicit exception as if something goes wrong, we'll find out anyway.
     */
    @Test(timeout=300_000)
	fun generateIRS() {
        // Tests aren't allowed to return things
        generateIRSTxn(1)
    }

    /**
     * Testing a simple IRS, add a few fixings and then display as CSV.
     */
    @Test(timeout=300_000)
	fun `IRS Export test`() {
        // No transactions etc required - we're just checking simple maths and export functionallity
        val irs = singleIRS(2)

        var newCalculation = irs.calculation

        val fixings = mapOf(LocalDate.of(2015, 3, 6) to "0.6",
                LocalDate.of(2015, 6, 8) to "0.75",
                LocalDate.of(2015, 9, 8) to "0.8",
                LocalDate.of(2015, 12, 8) to "0.55",
                LocalDate.of(2016, 3, 8) to "0.644")

        for ((key, value) in fixings) {
            newCalculation = newCalculation.applyFixing(key, FixedRate(PercentageRatioUnit(value)))
        }

        val newIRS = InterestRateSwap.State(irs.fixedLeg, irs.floatingLeg, newCalculation, irs.common, DUMMY_PARTY)
        println(newIRS.exportIRSToCSV())
    }

    /**
     * Make sure it has a schedule and the schedule has some unfixed rates.
     */
    @Test(timeout=300_000)
	fun `next fixing date`() {
        val irs = singleIRS(1)
        println(irs.calculation.nextFixingDate())
    }

    /**
     * Iterate through all the fix dates and add something.
     */
    @Test(timeout=300_000)
	fun generateIRSandFixSome() {
        val services = MockServices(listOf("net.corda.irs.contract"), MEGA_CORP.name,
                mock<IdentityService>().also {
                    listOf(MEGA_CORP, MINI_CORP).forEach { party ->
                        doReturn(party).whenever(it).partyFromKey(party.owningKey)
                    }
                },
                networkParameters = ledgerServices.networkParameters)
        var previousTXN = generateIRSTxn(1)
        previousTXN.toLedgerTransaction(services).verify()
        services.recordTransactions(previousTXN)
        fun currentIRS() = previousTXN.tx.outputsOfType<InterestRateSwap.State>().single()

        while (true) {
            val nextFix: FixOf = currentIRS().nextFixingOf() ?: break
            val fixTX: SignedTransaction = run {
                val tx = TransactionBuilder(DUMMY_NOTARY)
                val fixing = Fix(nextFix, "0.052".percent.value)
                InterestRateSwap().generateFix(tx, previousTXN.tx.outRef(0), fixing)
                tx.setTimeWindow(TEST_TX_TIME, 30.seconds)
                val ptx1 = megaCorpServices.signInitialTransaction(tx)
                val ptx2 = miniCorpServices.addSignature(ptx1)
                notaryServices.addSignature(ptx2)
            }
            fixTX.toLedgerTransaction(services).verify()
            services.recordTransactions(fixTX)
            previousTXN = fixTX
        }
    }

    // Move these later as they aren't IRS specific.
    @Test(timeout=300_000)
	fun `test some rate objects 100 * FixedRate(5%)`() {
        val r1 = FixedRate(PercentageRatioUnit("5"))
        assertEquals(5, 100 * r1)
    }

    @Test(timeout=300_000)
	fun `expression calculation testing`() {
        val dummyIRS = singleIRS()
        val stuffToPrint: ArrayList<String> = arrayListOf(
                "fixedLeg.notional.quantity",
                "fixedLeg.fixedRate.ratioUnit",
                "fixedLeg.fixedRate.ratioUnit.value",
                "floatingLeg.notional.quantity",
                "fixedLeg.fixedRate",
                "currentBusinessDate",
                "calculation.floatingLegPaymentSchedule.get(currentBusinessDate)",
                "fixedLeg.notional.token.currencyCode",
                "fixedLeg.notional.quantity * 10",
                "fixedLeg.notional.quantity * fixedLeg.fixedRate.ratioUnit.value",
                "(fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360 ",
                "(fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value))"
                // "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate"
                // "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value",
                //"( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) - (floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
                // "( fixedLeg.notional * fixedLeg.fixedRate )"
        )

        for (i in stuffToPrint) {
            println(i)
            val z = dummyIRS.evaluateCalculation(LocalDate.of(2016, 9, 15), Expression(i))
            println(z.javaClass)
            println(z)
            println("-----------")
        }
        // This does not throw an exception in the test itself; it evaluates the above and they will throw if they do not pass.
    }

    /**
     * Generates a typical transactional history for an IRS.
     */
    fun trade(): LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter> {

        val ld = LocalDate.of(2016, 3, 8)
        val bd = BigDecimal("0.0063518")
        return ledgerServices.ledger(DUMMY_NOTARY) {
            transaction("Agreement") {
                attachments(IRS_PROGRAM_ID)
                output(IRS_PROGRAM_ID, "irs post agreement", singleIRS())
                command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
                timeWindow(TEST_TX_TIME)
                this.verifies()
            }

            transaction("Fix") {
                attachments(IRS_PROGRAM_ID)
                input("irs post agreement")
                val postAgreement = "irs post agreement".output<InterestRateSwap.State>()
                output(IRS_PROGRAM_ID, "irs post first fixing",
                        postAgreement.copy(
                                postAgreement.fixedLeg,
                                postAgreement.floatingLeg,
                                postAgreement.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))),
                                postAgreement.common))
                command(ORACLE_PUBKEY,
                        InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)))
                timeWindow(TEST_TX_TIME)
                this.verifies()
            }
        }
    }

    private fun transaction(script: TransactionDSL<TransactionDSLInterpreter>.() -> EnforceVerifyOrFail) = run {
        ledgerServices.transaction(DUMMY_NOTARY, script)
    }

    @Test(timeout=300_000)
	fun `ensure failure occurs when there are inbound states for an agreement command`() {
        val irs = singleIRS()
        transaction {
            attachments(IRS_PROGRAM_ID)
            input(IRS_PROGRAM_ID, irs)
            output(IRS_PROGRAM_ID, "irs post agreement", irs)
            command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
            timeWindow(TEST_TX_TIME)
            this `fails with` "There are no in states for an agreement"
        }
    }

    @Test(timeout=300_000)
	fun `ensure failure occurs when no events in fix schedule`() {
        val irs = singleIRS()
        val emptySchedule = mutableMapOf<LocalDate, FixedRatePaymentEvent>()
        transaction {
            attachments(IRS_PROGRAM_ID)
            output(IRS_PROGRAM_ID, irs.copy(calculation = irs.calculation.copy(fixedLegPaymentSchedule = emptySchedule)))
            command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
            timeWindow(TEST_TX_TIME)
            this `fails with` "There are events in the fix schedule"
        }
    }

    @Test(timeout=300_000)
	fun `ensure failure occurs when no events in floating schedule`() {
        val irs = singleIRS()
        val emptySchedule = mutableMapOf<LocalDate, FloatingRatePaymentEvent>()
        transaction {
            attachments(IRS_PROGRAM_ID)
            output(IRS_PROGRAM_ID, irs.copy(calculation = irs.calculation.copy(floatingLegPaymentSchedule = emptySchedule)))
            command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
            timeWindow(TEST_TX_TIME)
            this `fails with` "There are events in the float schedule"
        }
    }

    @Test(timeout=300_000)
	fun `ensure notionals are non zero`() {
        val irs = singleIRS()
        transaction {
            attachments(IRS_PROGRAM_ID)
            output(IRS_PROGRAM_ID, irs.copy(irs.fixedLeg.copy(notional = irs.fixedLeg.notional.copy(quantity = 0))))
            command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
            timeWindow(TEST_TX_TIME)
            this `fails with` "All notionals must be non zero"
        }

        transaction {
            attachments(IRS_PROGRAM_ID)
            output(IRS_PROGRAM_ID, irs.copy(irs.fixedLeg.copy(notional = irs.floatingLeg.notional.copy(quantity = 0))))
            command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
            timeWindow(TEST_TX_TIME)
            this `fails with` "All notionals must be non zero"
        }
    }

    @Test(timeout=300_000)
	fun `ensure positive rate on fixed leg`() {
        val irs = singleIRS()
        val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(fixedRate = FixedRate(PercentageRatioUnit("-0.1"))))
        transaction {
            attachments(IRS_PROGRAM_ID)
            output(IRS_PROGRAM_ID, modifiedIRS)
            command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
            timeWindow(TEST_TX_TIME)
            this `fails with` "The fixed leg rate must be positive"
        }
    }

    /**
     * This will be modified once we adapt the IRS to be cross currency.
     */
    @Test(timeout=300_000)
	fun `ensure same currency notionals`() {
        val irs = singleIRS()
        val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.fixedLeg.notional.quantity, Currency.getInstance("JPY"))))
        transaction {
            attachments(IRS_PROGRAM_ID)
            output(IRS_PROGRAM_ID, modifiedIRS)
            command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
            timeWindow(TEST_TX_TIME)
            this `fails with` "The currency of the notionals must be the same"
        }
    }

    @Test(timeout=300_000)
	fun `ensure notional amounts are equal`() {
        val irs = singleIRS()
        val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.floatingLeg.notional.quantity + 1, irs.floatingLeg.notional.token)))
        transaction {
            attachments(IRS_PROGRAM_ID)
            output(IRS_PROGRAM_ID, modifiedIRS)
            command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
            timeWindow(TEST_TX_TIME)
            this `fails with` "All leg notionals must be the same"
        }
    }

    @Test(timeout=300_000)
	fun `ensure trade date and termination date checks are done pt1`() {
        val irs = singleIRS()
        val modifiedIRS1 = irs.copy(fixedLeg = irs.fixedLeg.copy(terminationDate = irs.fixedLeg.effectiveDate.minusDays(1)))
        transaction {
            attachments(IRS_PROGRAM_ID)
            output(IRS_PROGRAM_ID, modifiedIRS1)
            command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
            timeWindow(TEST_TX_TIME)
            this `fails with` "The effective date is before the termination date for the fixed leg"
        }

        val modifiedIRS2 = irs.copy(floatingLeg = irs.floatingLeg.copy(terminationDate = irs.floatingLeg.effectiveDate.minusDays(1)))
        transaction {
            attachments(IRS_PROGRAM_ID)
            output(IRS_PROGRAM_ID, modifiedIRS2)
            command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
            timeWindow(TEST_TX_TIME)
            this `fails with` "The effective date is before the termination date for the floating leg"
        }
    }

    @Test(timeout=300_000)
	fun `ensure trade date and termination date checks are done pt2`() {
        val irs = singleIRS()

        val modifiedIRS3 = irs.copy(floatingLeg = irs.floatingLeg.copy(terminationDate = irs.fixedLeg.terminationDate.minusDays(1)))
        transaction {
            attachments(IRS_PROGRAM_ID)
            output(IRS_PROGRAM_ID, modifiedIRS3)
            command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
            timeWindow(TEST_TX_TIME)
            this `fails with` "The termination dates are aligned"
        }

        val modifiedIRS4 = irs.copy(floatingLeg = irs.floatingLeg.copy(effectiveDate = irs.fixedLeg.effectiveDate.minusDays(1)))
        transaction {
            attachments(IRS_PROGRAM_ID)
            output(IRS_PROGRAM_ID, modifiedIRS4)
            command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
            timeWindow(TEST_TX_TIME)
            this `fails with` "The effective dates are aligned"
        }
    }

    @Test(timeout=300_000)
	fun `various fixing tests`() {
        val ld = LocalDate.of(2016, 3, 8)
        val bd = BigDecimal("0.0063518")

        transaction {
            attachments(IRS_PROGRAM_ID)
            output(IRS_PROGRAM_ID, "irs post agreement", singleIRS())
            command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
            timeWindow(TEST_TX_TIME)
            this.verifies()
        }

        val oldIRS = singleIRS(1)
        val newIRS = oldIRS.copy(oldIRS.fixedLeg,
                oldIRS.floatingLeg,
                oldIRS.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))),
                oldIRS.common)

        transaction {
            attachments(IRS_PROGRAM_ID)
            input(IRS_PROGRAM_ID, oldIRS)

            // Templated tweak for reference. A corrent fixing applied should be ok
            tweak {
                command(ORACLE_PUBKEY,
                        InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)))
                timeWindow(TEST_TX_TIME)
                output(IRS_PROGRAM_ID, newIRS)
                this.verifies()
            }

            // This test makes sure that verify confirms the fixing was applied and there is a difference in the old and new
            tweak {
                command(ORACLE_PUBKEY, InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)))
                timeWindow(TEST_TX_TIME)
                output(IRS_PROGRAM_ID, oldIRS)
                this `fails with` "There is at least one difference in the IRS floating leg payment schedules"
            }

            // This tests tries to sneak in a change to another fixing (which may or may not be the latest one)
            tweak {
                command(ORACLE_PUBKEY, InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)))
                timeWindow(TEST_TX_TIME)

                val firstResetKey = newIRS.calculation.floatingLegPaymentSchedule.keys.toList()[1]
                val firstResetValue = newIRS.calculation.floatingLegPaymentSchedule[firstResetKey]
                val modifiedFirstResetValue = firstResetValue!!.copy(notional = Amount(firstResetValue.notional.quantity, Currency.getInstance("JPY")))
                output(IRS_PROGRAM_ID,
                        newIRS.copy(
                                newIRS.fixedLeg,
                                newIRS.floatingLeg,
                                newIRS.calculation.copy(floatingLegPaymentSchedule = newIRS.calculation.floatingLegPaymentSchedule.plus(
                                        Pair(firstResetKey, modifiedFirstResetValue))),
                                newIRS.common))
                this `fails with` "There is only one change in the IRS floating leg payment schedule"
            }

            // This tests modifies the payment currency for the fixing
            tweak {
                command(ORACLE_PUBKEY, InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)))
                timeWindow(TEST_TX_TIME)

                val latestReset = newIRS.calculation.floatingLegPaymentSchedule.filter { it.value.rate is FixedRate }.maxBy { it.key }
                val modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.quantity, Currency.getInstance("JPY")))
                output(IRS_PROGRAM_ID,
                        newIRS.copy(
                                newIRS.fixedLeg,
                                newIRS.floatingLeg,
                                newIRS.calculation.copy(floatingLegPaymentSchedule = newIRS.calculation.floatingLegPaymentSchedule.plus(
                                        Pair(latestReset.key, modifiedLatestResetValue))),
                                newIRS.common))
                this `fails with` "The fix payment has the same currency as the notional"
            }
        }
    }

    /**
     * This returns an example of transactions that are grouped by TradeId and then a fixing applied.
     * It's important to make the tradeID different for two reasons, the hashes will be the same and all sorts of confusion will
     * result and the grouping won't work either.
     * In reality, the only fields that should be in common will be the next fixing date and the reference rate.
     */
    fun tradegroups(): LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter> {
        val ld1 = LocalDate.of(2016, 3, 8)
        val bd1 = BigDecimal("0.0063518")

        val irs = singleIRS()
        return ledgerServices.ledger(DUMMY_NOTARY) {
            transaction("Agreement") {
                attachments(IRS_PROGRAM_ID)
                output(IRS_PROGRAM_ID, "irs post agreement1",
                        irs.copy(
                                irs.fixedLeg,
                                irs.floatingLeg,
                                irs.calculation,
                                irs.common.copy(tradeID = "t1")))
                command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
                timeWindow(TEST_TX_TIME)
                this.verifies()
            }

            transaction("Agreement") {
                attachments(IRS_PROGRAM_ID)
                output(IRS_PROGRAM_ID, "irs post agreement2",
                        irs.copy(
                                linearId = UniqueIdentifier("t2"),
                                fixedLeg = irs.fixedLeg,
                                floatingLeg = irs.floatingLeg,
                                calculation = irs.calculation,
                                common = irs.common.copy(tradeID = "t2")))
                command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
                timeWindow(TEST_TX_TIME)
                this.verifies()
            }

            transaction("Fix") {
                attachments(IRS_PROGRAM_ID)
                input("irs post agreement1")
                input("irs post agreement2")
                val postAgreement1 = "irs post agreement1".output<InterestRateSwap.State>()
                output(IRS_PROGRAM_ID, "irs post first fixing1",
                        postAgreement1.copy(
                                postAgreement1.fixedLeg,
                                postAgreement1.floatingLeg,
                                postAgreement1.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
                                postAgreement1.common.copy(tradeID = "t1")))
                val postAgreement2 = "irs post agreement2".output<InterestRateSwap.State>()
                output(IRS_PROGRAM_ID, "irs post first fixing2",
                        postAgreement2.copy(
                                postAgreement2.fixedLeg,
                                postAgreement2.floatingLeg,
                                postAgreement2.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
                                postAgreement2.common.copy(tradeID = "t2")))
                command(ORACLE_PUBKEY,
                        InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld1, Tenor("3M")), bd1)))
                timeWindow(TEST_TX_TIME)
                this.verifies()
            }
        }
    }
}
