import invariant from 'tiny-invariant'
import JSBI from 'jsbi'
import { ChainId, WETH as _WETH, TradeType, Rounding, Token, TokenAmount, Pair, Route, Trade } from '../src'

const ADDRESSES = [
  '0x0000000000000000000000000000000000000001',
  '0x0000000000000000000000000000000000000002',
  '0x0000000000000000000000000000000000000003'
]
const CHAIN_ID = ChainId.TESTNET
const WETH = _WETH[ChainId.TESTNET]
const DECIMAL_PERMUTATIONS: [number, number, number][] = [
  [0, 0, 0],
  [0, 9, 18],
  [18, 18, 18]
]

function decimalize(amount: number, decimals: number): JSBI {
  return JSBI.multiply(JSBI.BigInt(amount), JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(decimals)))
}

describe('entities', () => {
  DECIMAL_PERMUTATIONS.forEach(decimals => {
    describe(`decimals permutation: ${decimals}`, () => {
      let tokens: Token[]
      it('Token', () => {
        tokens = ADDRESSES.map((address, i) => new Token(CHAIN_ID, address, decimals[i]))
        tokens.forEach((token, i) => {
          expect(token.chainId).toEqual(CHAIN_ID)
          expect(token.address).toEqual(ADDRESSES[i])
          expect(token.decimals).toEqual(decimals[i])
        })
      })

      let pairs: Pair[]
      it('Pair', () => {
        pairs = [
          new Pair(
            new TokenAmount(tokens[0], decimalize(1, tokens[0].decimals)),
            new TokenAmount(tokens[1], decimalize(1, tokens[1].decimals))
          ),
          new Pair(
            new TokenAmount(tokens[1], decimalize(1, tokens[1].decimals)),
            new TokenAmount(tokens[2], decimalize(1, tokens[2].decimals))
          ),
          new Pair(
            new TokenAmount(tokens[2], decimalize(1, tokens[2].decimals)),
            new TokenAmount(WETH, decimalize(1234, WETH.decimals))
          )
        ]
      })

      let route: Route
      it('Route', () => {
        route = new Route(pairs, tokens[0])
        expect(route.pairs).toEqual(pairs)
        expect(route.path).toEqual(tokens.concat([WETH]))
        expect(route.input).toEqual(tokens[0])
        expect(route.output).toEqual(WETH)
      })

      it('Price:Route.midPrice', () => {
        invariant(route.input instanceof Token)
        invariant(route.output instanceof Token)
        expect(route.midPrice.quote(new TokenAmount(route.input, decimalize(1, route.input.decimals)))).toEqual(
          new TokenAmount(route.output, decimalize(1234, route.output.decimals))
        )
        expect(
          route.midPrice.invert().quote(new TokenAmount(route.output, decimalize(1234, route.output.decimals)))
        ).toEqual(new TokenAmount(route.input, decimalize(1, route.input.decimals)))

        expect(route.midPrice.toSignificant(1)).toEqual('1000')
        expect(route.midPrice.toSignificant(2)).toEqual('1200')
        expect(route.midPrice.toSignificant(3)).toEqual('1230')
        expect(route.midPrice.toSignificant(4)).toEqual('1234')
        expect(route.midPrice.toSignificant(5)).toEqual('1234')
        expect(route.midPrice.toSignificant(5, { groupSeparator: ',' })).toEqual('1,234')
        expect(route.midPrice.invert().toSignificant(1)).toEqual('0.0008')
        expect(route.midPrice.invert().toSignificant(2)).toEqual('0.00081')
        expect(route.midPrice.invert().toSignificant(3)).toEqual('0.00081')
        expect(route.midPrice.invert().toSignificant(4)).toEqual('0.0008104')
        expect(route.midPrice.invert().toSignificant(4, undefined, Rounding.ROUND_DOWN)).toEqual('0.0008103')
        expect(route.midPrice.invert().toSignificant(5)).toEqual('0.00081037')

        expect(route.midPrice.toFixed(0)).toEqual('1234')
        expect(route.midPrice.toFixed(1)).toEqual('1234.0')
        expect(route.midPrice.toFixed(2)).toEqual('1234.00')
        expect(route.midPrice.toFixed(2, { groupSeparator: ',' })).toEqual('1,234.00')
        expect(route.midPrice.invert().toFixed(0)).toEqual('0')
        expect(route.midPrice.invert().toFixed(1)).toEqual('0.0')
        expect(route.midPrice.invert().toFixed(2)).toEqual('0.00')
        expect(route.midPrice.invert().toFixed(3)).toEqual('0.001')
        expect(route.midPrice.invert().toFixed(4)).toEqual('0.0008')
        expect(route.midPrice.invert().toFixed(5)).toEqual('0.00081')
        expect(route.midPrice.invert().toFixed(6)).toEqual('0.000810')
        expect(route.midPrice.invert().toFixed(7)).toEqual('0.0008104')
        expect(route.midPrice.invert().toFixed(7, undefined, Rounding.ROUND_DOWN)).toEqual('0.0008103')
        expect(route.midPrice.invert().toFixed(8)).toEqual('0.00081037')
      })

      describe('Trade', () => {
        let route: Route
        it('TradeType.EXACT_INPUT', () => {
          route = new Route(
            [
              new Pair(
                new TokenAmount(tokens[1], decimalize(5, tokens[1].decimals)),
                new TokenAmount(WETH, decimalize(10, WETH.decimals))
              )
            ],
            tokens[1]
          )
          const inputAmount = new TokenAmount(tokens[1], decimalize(1, tokens[1].decimals))
          const expectedOutputAmount = new TokenAmount(WETH, '1663192997082117548')
          const trade = new Trade(route, inputAmount, TradeType.EXACT_INPUT)
          expect(trade.route).toEqual(route)
          expect(trade.tradeType).toEqual(TradeType.EXACT_INPUT)
          expect(trade.inputAmount).toEqual(inputAmount)
          expect(trade.outputAmount).toEqual(expectedOutputAmount)

          expect(trade.executionPrice.toSignificant(18)).toEqual('1.66319299708211755')
          expect(trade.executionPrice.invert().toSignificant(18)).toEqual('0.601253132832080201')
          expect(trade.executionPrice.quote(inputAmount)).toEqual(expectedOutputAmount)
          expect(trade.executionPrice.invert().quote(expectedOutputAmount)).toEqual(inputAmount)

          expect(trade.nextMidPrice.toSignificant(18)).toEqual('1.38946783381964708')
          expect(trade.nextMidPrice.invert().toSignificant(18)).toEqual('0.7197')

          expect(trade.priceImpact.toSignificant(18)).toEqual('16.8403501458941226')
        })

        it('TradeType.EXACT_OUTPUT', () => {
          const outputAmount = new TokenAmount(WETH, '1663192997082117548')
          const expectedInputAmount = new TokenAmount(tokens[1], decimalize(1, tokens[1].decimals))
          const trade = new Trade(route, outputAmount, TradeType.EXACT_OUTPUT)
          expect(trade.route).toEqual(route)
          expect(trade.tradeType).toEqual(TradeType.EXACT_OUTPUT)
          expect(trade.outputAmount).toEqual(outputAmount)
          expect(trade.inputAmount).toEqual(expectedInputAmount)

          expect(trade.executionPrice.toSignificant(18)).toEqual('1.66319299708211755')
          expect(trade.executionPrice.invert().toSignificant(18)).toEqual('0.601253132832080201')
          expect(trade.executionPrice.quote(expectedInputAmount)).toEqual(outputAmount)
          expect(trade.executionPrice.invert().quote(outputAmount)).toEqual(expectedInputAmount)

          expect(trade.nextMidPrice.toSignificant(18)).toEqual('1.38946783381964708')
          expect(trade.nextMidPrice.invert().toSignificant(18)).toEqual('0.7197')

          expect(trade.priceImpact.toSignificant(18)).toEqual('16.8403501458941226')
        })

        it('minimum TradeType.EXACT_INPUT', () => {
          if ([9, 18].includes(tokens[1].decimals)) {
            const route = new Route(
              [
                new Pair(
                  new TokenAmount(tokens[1], decimalize(1, tokens[1].decimals)),
                  new TokenAmount(
                    WETH,
                    JSBI.add(
                      decimalize(10, WETH.decimals),

                      tokens[1].decimals === 9 ? JSBI.BigInt('30090280812437312') : JSBI.BigInt('30090270812437322')
                    )
                  )
                )
              ],
              tokens[1],
              WETH
            )
            const outputAmount = new TokenAmount(tokens[1], '1')
            const trade = new Trade(route, outputAmount, TradeType.EXACT_INPUT)

            expect(trade.priceImpact.toSignificant(18)).toEqual(
              tokens[1].decimals === 9 ? '0.2500001008007499' : '0.3000000000000001'
            )
          }
        })
      })

      it('TokenAmount', () => {
        const amount = new TokenAmount(WETH, '1234567000000000000000')
        expect(amount.toExact()).toEqual('1234.567')
        expect(amount.toExact({ groupSeparator: ',' })).toEqual('1,234.567')
      })
    })
  })
})
