import { BigNumber, BigNumberish, Wallet } from 'ethers'
import { ethers, waffle } from 'hardhat'
import { OracleTest } from '../typechain/OracleTest'
import checkObservationEquals from './shared/checkObservationEquals'
import { expect } from './shared/expect'
import { TEST_POOL_START_TIME } from './shared/fixtures'
import snapshotGasCost from './shared/snapshotGasCost'
import { MaxUint128 } from './shared/utilities'

describe('Oracle', () => {
  let wallet: Wallet, other: Wallet

  let loadFixture: ReturnType<typeof waffle.createFixtureLoader>
  before('create fixture loader', async () => {
    ;[wallet, other] = await (ethers as any).getSigners()
    loadFixture = waffle.createFixtureLoader([wallet, other])
  })

  const oracleFixture = async () => {
    const oracleTestFactory = await ethers.getContractFactory('OracleTest')
    return (await oracleTestFactory.deploy()) as OracleTest
  }

  const initializedOracleFixture = async () => {
    const oracle = await oracleFixture()
    await oracle.initialize({
      time: 0,
      tick: 0,
      liquidity: 0,
    })
    return oracle
  }

  describe('#initialize', () => {
    let oracle: OracleTest
    beforeEach('deploy test oracle', async () => {
      oracle = await loadFixture(oracleFixture)
    })
    it('index is 0', async () => {
      await oracle.initialize({ liquidity: 1, tick: 1, time: 1 })
      expect(await oracle.index()).to.eq(0)
    })
    it('cardinality is 1', async () => {
      await oracle.initialize({ liquidity: 1, tick: 1, time: 1 })
      expect(await oracle.cardinality()).to.eq(1)
    })
    it('cardinality next is 1', async () => {
      await oracle.initialize({ liquidity: 1, tick: 1, time: 1 })
      expect(await oracle.cardinalityNext()).to.eq(1)
    })
    it('sets first slot timestamp only', async () => {
      await oracle.initialize({ liquidity: 1, tick: 1, time: 1 })
      checkObservationEquals(await oracle.observations(0), {
        initialized: true,
        blockTimestamp: 1,
        tickCumulative: 0,
        secondsPerLiquidityCumulativeX128: 0,
      })
    })
    it('gas', async () => {
      await snapshotGasCost(oracle.initialize({ liquidity: 1, tick: 1, time: 1 }))
    })
  })

  describe('#grow', () => {
    let oracle: OracleTest
    beforeEach('deploy initialized test oracle', async () => {
      oracle = await loadFixture(initializedOracleFixture)
    })

    it('increases the cardinality next for the first call', async () => {
      await oracle.grow(5)
      expect(await oracle.index()).to.eq(0)
      expect(await oracle.cardinality()).to.eq(1)
      expect(await oracle.cardinalityNext()).to.eq(5)
    })

    it('does not touch the first slot', async () => {
      await oracle.grow(5)
      checkObservationEquals(await oracle.observations(0), {
        secondsPerLiquidityCumulativeX128: 0,
        tickCumulative: 0,
        blockTimestamp: 0,
        initialized: true,
      })
    })

    it('is no op if oracle is already gte that size', async () => {
      await oracle.grow(5)
      await oracle.grow(3)
      expect(await oracle.index()).to.eq(0)
      expect(await oracle.cardinality()).to.eq(1)
      expect(await oracle.cardinalityNext()).to.eq(5)
    })

    it('adds data to all the slots', async () => {
      await oracle.grow(5)
      for (let i = 1; i < 5; i++) {
        checkObservationEquals(await oracle.observations(i), {
          secondsPerLiquidityCumulativeX128: 0,
          tickCumulative: 0,
          blockTimestamp: 1,
          initialized: false,
        })
      }
    })

    it('grow after wrap', async () => {
      await oracle.grow(2)
      await oracle.update({ advanceTimeBy: 2, liquidity: 1, tick: 1 }) // index is now 1
      await oracle.update({ advanceTimeBy: 2, liquidity: 1, tick: 1 }) // index is now 0 again
      expect(await oracle.index()).to.eq(0)
      await oracle.grow(3)
      expect(await oracle.index()).to.eq(0)
      expect(await oracle.cardinality()).to.eq(2)
      expect(await oracle.cardinalityNext()).to.eq(3)
    })

    it('gas for growing by 1 slot when index == cardinality - 1', async () => {
      await snapshotGasCost(oracle.grow(2))
    })

    it('gas for growing by 10 slots when index == cardinality - 1', async () => {
      await snapshotGasCost(oracle.grow(11))
    })

    it('gas for growing by 1 slot when index != cardinality - 1', async () => {
      await oracle.grow(2)
      await snapshotGasCost(oracle.grow(3))
    })

    it('gas for growing by 10 slots when index != cardinality - 1', async () => {
      await oracle.grow(2)
      await snapshotGasCost(oracle.grow(12))
    })
  })

  describe('#write', () => {
    let oracle: OracleTest

    beforeEach('deploy initialized test oracle', async () => {
      oracle = await loadFixture(initializedOracleFixture)
    })

    it('single element array gets overwritten', async () => {
      await oracle.update({ advanceTimeBy: 1, tick: 2, liquidity: 5 })
      expect(await oracle.index()).to.eq(0)
      checkObservationEquals(await oracle.observations(0), {
        initialized: true,
        secondsPerLiquidityCumulativeX128: '340282366920938463463374607431768211456',
        tickCumulative: 0,
        blockTimestamp: 1,
      })
      await oracle.update({ advanceTimeBy: 5, tick: -1, liquidity: 8 })
      expect(await oracle.index()).to.eq(0)
      checkObservationEquals(await oracle.observations(0), {
        initialized: true,
        secondsPerLiquidityCumulativeX128: '680564733841876926926749214863536422912',
        tickCumulative: 10,
        blockTimestamp: 6,
      })
      await oracle.update({ advanceTimeBy: 3, tick: 2, liquidity: 3 })
      expect(await oracle.index()).to.eq(0)
      checkObservationEquals(await oracle.observations(0), {
        initialized: true,
        secondsPerLiquidityCumulativeX128: '808170621437228850725514692650449502208',
        tickCumulative: 7,
        blockTimestamp: 9,
      })
    })

    it('does nothing if time has not changed', async () => {
      await oracle.grow(2)
      await oracle.update({ advanceTimeBy: 1, tick: 3, liquidity: 2 })
      expect(await oracle.index()).to.eq(1)
      await oracle.update({ advanceTimeBy: 0, tick: -5, liquidity: 9 })
      expect(await oracle.index()).to.eq(1)
    })

    it('writes an index if time has changed', async () => {
      await oracle.grow(3)
      await oracle.update({ advanceTimeBy: 6, tick: 3, liquidity: 2 })
      expect(await oracle.index()).to.eq(1)
      await oracle.update({ advanceTimeBy: 4, tick: -5, liquidity: 9 })

      expect(await oracle.index()).to.eq(2)
      checkObservationEquals(await oracle.observations(1), {
        tickCumulative: 0,
        secondsPerLiquidityCumulativeX128: '2041694201525630780780247644590609268736',
        initialized: true,
        blockTimestamp: 6,
      })
    })

    it('grows cardinality when writing past', async () => {
      await oracle.grow(2)
      await oracle.grow(4)
      expect(await oracle.cardinality()).to.eq(1)
      await oracle.update({ advanceTimeBy: 3, tick: 5, liquidity: 6 })
      expect(await oracle.cardinality()).to.eq(4)
      await oracle.update({ advanceTimeBy: 4, tick: 6, liquidity: 4 })
      expect(await oracle.cardinality()).to.eq(4)
      expect(await oracle.index()).to.eq(2)
      checkObservationEquals(await oracle.observations(2), {
        secondsPerLiquidityCumulativeX128: '1247702012043441032699040227249816775338',
        tickCumulative: 20,
        initialized: true,
        blockTimestamp: 7,
      })
    })

    it('wraps around', async () => {
      await oracle.grow(3)
      await oracle.update({ advanceTimeBy: 3, tick: 1, liquidity: 2 })
      await oracle.update({ advanceTimeBy: 4, tick: 2, liquidity: 3 })
      await oracle.update({ advanceTimeBy: 5, tick: 3, liquidity: 4 })

      expect(await oracle.index()).to.eq(0)

      checkObservationEquals(await oracle.observations(0), {
        secondsPerLiquidityCumulativeX128: '2268549112806256423089164049545121409706',
        tickCumulative: 14,
        initialized: true,
        blockTimestamp: 12,
      })
    })

    it('accumulates liquidity', async () => {
      await oracle.grow(4)

      await oracle.update({ advanceTimeBy: 3, tick: 3, liquidity: 2 })
      await oracle.update({ advanceTimeBy: 4, tick: -7, liquidity: 6 })
      await oracle.update({ advanceTimeBy: 5, tick: -2, liquidity: 4 })

      expect(await oracle.index()).to.eq(3)

      checkObservationEquals(await oracle.observations(1), {
        initialized: true,
        tickCumulative: 0,
        secondsPerLiquidityCumulativeX128: '1020847100762815390390123822295304634368',
        blockTimestamp: 3,
      })
      checkObservationEquals(await oracle.observations(2), {
        initialized: true,
        tickCumulative: 12,
        secondsPerLiquidityCumulativeX128: '1701411834604692317316873037158841057280',
        blockTimestamp: 7,
      })
      checkObservationEquals(await oracle.observations(3), {
        initialized: true,
        tickCumulative: -23,
        secondsPerLiquidityCumulativeX128: '1984980473705474370203018543351981233493',
        blockTimestamp: 12,
      })
      checkObservationEquals(await oracle.observations(4), {
        initialized: false,
        tickCumulative: 0,
        secondsPerLiquidityCumulativeX128: 0,
        blockTimestamp: 0,
      })
    })
  })

  describe('#observe', () => {
    describe('before initialization', async () => {
      let oracle: OracleTest
      beforeEach('deploy test oracle', async () => {
        oracle = await loadFixture(oracleFixture)
      })

      const observeSingle = async (secondsAgo: number) => {
        const {
          tickCumulatives: [tickCumulative],
          secondsPerLiquidityCumulativeX128s: [secondsPerLiquidityCumulativeX128],
        } = await oracle.observe([secondsAgo])
        return { secondsPerLiquidityCumulativeX128, tickCumulative }
      }

      it('fails before initialize', async () => {
        await expect(observeSingle(0)).to.be.revertedWith('I')
      })

      it('fails if an older observation does not exist', async () => {
        await oracle.initialize({ liquidity: 4, tick: 2, time: 5 })
        await expect(observeSingle(1)).to.be.revertedWith('OLD')
      })

      it('does not fail across overflow boundary', async () => {
        await oracle.initialize({ liquidity: 4, tick: 2, time: 2 ** 32 - 1 })
        await oracle.advanceTime(2)
        const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(1)
        expect(tickCumulative).to.be.eq(2)
        expect(secondsPerLiquidityCumulativeX128).to.be.eq('85070591730234615865843651857942052864')
      })

      it('interpolates correctly at max liquidity', async () => {
        await oracle.initialize({ liquidity: MaxUint128, tick: 0, time: 0 })
        await oracle.grow(2)
        await oracle.update({ advanceTimeBy: 13, tick: 0, liquidity: 0 })
        let { secondsPerLiquidityCumulativeX128 } = await observeSingle(0)
        expect(secondsPerLiquidityCumulativeX128).to.eq(13)
        ;({ secondsPerLiquidityCumulativeX128 } = await observeSingle(6))
        expect(secondsPerLiquidityCumulativeX128).to.eq(7)
        ;({ secondsPerLiquidityCumulativeX128 } = await observeSingle(12))
        expect(secondsPerLiquidityCumulativeX128).to.eq(1)
        ;({ secondsPerLiquidityCumulativeX128 } = await observeSingle(13))
        expect(secondsPerLiquidityCumulativeX128).to.eq(0)
      })

      it('interpolates correctly at min liquidity', async () => {
        await oracle.initialize({ liquidity: 0, tick: 0, time: 0 })
        await oracle.grow(2)
        await oracle.update({ advanceTimeBy: 13, tick: 0, liquidity: MaxUint128 })
        let { secondsPerLiquidityCumulativeX128 } = await observeSingle(0)
        expect(secondsPerLiquidityCumulativeX128).to.eq(BigNumber.from(13).shl(128))
        ;({ secondsPerLiquidityCumulativeX128 } = await observeSingle(6))
        expect(secondsPerLiquidityCumulativeX128).to.eq(BigNumber.from(7).shl(128))
        ;({ secondsPerLiquidityCumulativeX128 } = await observeSingle(12))
        expect(secondsPerLiquidityCumulativeX128).to.eq(BigNumber.from(1).shl(128))
        ;({ secondsPerLiquidityCumulativeX128 } = await observeSingle(13))
        expect(secondsPerLiquidityCumulativeX128).to.eq(0)
      })

      it('interpolates the same as 0 liquidity for 1 liquidity', async () => {
        await oracle.initialize({ liquidity: 1, tick: 0, time: 0 })
        await oracle.grow(2)
        await oracle.update({ advanceTimeBy: 13, tick: 0, liquidity: MaxUint128 })
        let { secondsPerLiquidityCumulativeX128 } = await observeSingle(0)
        expect(secondsPerLiquidityCumulativeX128).to.eq(BigNumber.from(13).shl(128))
        ;({ secondsPerLiquidityCumulativeX128 } = await observeSingle(6))
        expect(secondsPerLiquidityCumulativeX128).to.eq(BigNumber.from(7).shl(128))
        ;({ secondsPerLiquidityCumulativeX128 } = await observeSingle(12))
        expect(secondsPerLiquidityCumulativeX128).to.eq(BigNumber.from(1).shl(128))
        ;({ secondsPerLiquidityCumulativeX128 } = await observeSingle(13))
        expect(secondsPerLiquidityCumulativeX128).to.eq(0)
      })

      it('interpolates correctly across uint32 seconds boundaries', async () => {
        // setup
        await oracle.initialize({ liquidity: 0, tick: 0, time: 0 })
        await oracle.grow(2)
        await oracle.update({ advanceTimeBy: 2 ** 32 - 6, tick: 0, liquidity: 0 })
        let { secondsPerLiquidityCumulativeX128 } = await observeSingle(0)
        expect(secondsPerLiquidityCumulativeX128).to.eq(BigNumber.from(2 ** 32 - 6).shl(128))
        await oracle.update({ advanceTimeBy: 13, tick: 0, liquidity: 0 })
        ;({ secondsPerLiquidityCumulativeX128 } = await observeSingle(0))
        expect(secondsPerLiquidityCumulativeX128).to.eq(BigNumber.from(7).shl(128))

        // interpolation checks
        ;({ secondsPerLiquidityCumulativeX128 } = await observeSingle(3))
        expect(secondsPerLiquidityCumulativeX128).to.eq(BigNumber.from(4).shl(128))
        ;({ secondsPerLiquidityCumulativeX128 } = await observeSingle(8))
        expect(secondsPerLiquidityCumulativeX128).to.eq(BigNumber.from(2 ** 32 - 1).shl(128))
      })

      it('single observation at current time', async () => {
        await oracle.initialize({ liquidity: 4, tick: 2, time: 5 })
        const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(0)
        expect(tickCumulative).to.eq(0)
        expect(secondsPerLiquidityCumulativeX128).to.eq(0)
      })

      it('single observation in past but not earlier than secondsAgo', async () => {
        await oracle.initialize({ liquidity: 4, tick: 2, time: 5 })
        await oracle.advanceTime(3)
        await expect(observeSingle(4)).to.be.revertedWith('OLD')
      })

      it('single observation in past at exactly seconds ago', async () => {
        await oracle.initialize({ liquidity: 4, tick: 2, time: 5 })
        await oracle.advanceTime(3)
        const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(3)
        expect(tickCumulative).to.eq(0)
        expect(secondsPerLiquidityCumulativeX128).to.eq(0)
      })

      it('single observation in past counterfactual in past', async () => {
        await oracle.initialize({ liquidity: 4, tick: 2, time: 5 })
        await oracle.advanceTime(3)
        const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(1)
        expect(tickCumulative).to.eq(4)
        expect(secondsPerLiquidityCumulativeX128).to.eq('170141183460469231731687303715884105728')
      })

      it('single observation in past counterfactual now', async () => {
        await oracle.initialize({ liquidity: 4, tick: 2, time: 5 })
        await oracle.advanceTime(3)
        const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(0)
        expect(tickCumulative).to.eq(6)
        expect(secondsPerLiquidityCumulativeX128).to.eq('255211775190703847597530955573826158592')
      })

      it('two observations in chronological order 0 seconds ago exact', async () => {
        await oracle.initialize({ liquidity: 5, tick: -5, time: 5 })
        await oracle.grow(2)
        await oracle.update({ advanceTimeBy: 4, tick: 1, liquidity: 2 })
        const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(0)
        expect(tickCumulative).to.eq(-20)
        expect(secondsPerLiquidityCumulativeX128).to.eq('272225893536750770770699685945414569164')
      })

      it('two observations in chronological order 0 seconds ago counterfactual', async () => {
        await oracle.initialize({ liquidity: 5, tick: -5, time: 5 })
        await oracle.grow(2)
        await oracle.update({ advanceTimeBy: 4, tick: 1, liquidity: 2 })
        await oracle.advanceTime(7)
        const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(0)
        expect(tickCumulative).to.eq(-13)
        expect(secondsPerLiquidityCumulativeX128).to.eq('1463214177760035392892510811956603309260')
      })

      it('two observations in chronological order seconds ago is exactly on first observation', async () => {
        await oracle.initialize({ liquidity: 5, tick: -5, time: 5 })
        await oracle.grow(2)
        await oracle.update({ advanceTimeBy: 4, tick: 1, liquidity: 2 })
        await oracle.advanceTime(7)
        const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(11)
        expect(tickCumulative).to.eq(0)
        expect(secondsPerLiquidityCumulativeX128).to.eq(0)
      })

      it('two observations in chronological order seconds ago is between first and second', async () => {
        await oracle.initialize({ liquidity: 5, tick: -5, time: 5 })
        await oracle.grow(2)
        await oracle.update({ advanceTimeBy: 4, tick: 1, liquidity: 2 })
        await oracle.advanceTime(7)
        const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(9)
        expect(tickCumulative).to.eq(-10)
        expect(secondsPerLiquidityCumulativeX128).to.eq('136112946768375385385349842972707284582')
      })

      it('two observations in reverse order 0 seconds ago exact', async () => {
        await oracle.initialize({ liquidity: 5, tick: -5, time: 5 })
        await oracle.grow(2)
        await oracle.update({ advanceTimeBy: 4, tick: 1, liquidity: 2 })
        await oracle.update({ advanceTimeBy: 3, tick: -5, liquidity: 4 })
        const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(0)
        expect(tickCumulative).to.eq(-17)
        expect(secondsPerLiquidityCumulativeX128).to.eq('782649443918158465965761597093066886348')
      })

      it('two observations in reverse order 0 seconds ago counterfactual', async () => {
        await oracle.initialize({ liquidity: 5, tick: -5, time: 5 })
        await oracle.grow(2)
        await oracle.update({ advanceTimeBy: 4, tick: 1, liquidity: 2 })
        await oracle.update({ advanceTimeBy: 3, tick: -5, liquidity: 4 })
        await oracle.advanceTime(7)
        const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(0)
        expect(tickCumulative).to.eq(-52)
        expect(secondsPerLiquidityCumulativeX128).to.eq('1378143586029800777026667160098661256396')
      })

      it('two observations in reverse order seconds ago is exactly on first observation', async () => {
        await oracle.initialize({ liquidity: 5, tick: -5, time: 5 })
        await oracle.grow(2)
        await oracle.update({ advanceTimeBy: 4, tick: 1, liquidity: 2 })
        await oracle.update({ advanceTimeBy: 3, tick: -5, liquidity: 4 })
        await oracle.advanceTime(7)
        const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(10)
        expect(tickCumulative).to.eq(-20)
        expect(secondsPerLiquidityCumulativeX128).to.eq('272225893536750770770699685945414569164')
      })

      it('two observations in reverse order seconds ago is between first and second', async () => {
        await oracle.initialize({ liquidity: 5, tick: -5, time: 5 })
        await oracle.grow(2)
        await oracle.update({ advanceTimeBy: 4, tick: 1, liquidity: 2 })
        await oracle.update({ advanceTimeBy: 3, tick: -5, liquidity: 4 })
        await oracle.advanceTime(7)
        const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(9)
        expect(tickCumulative).to.eq(-19)
        expect(secondsPerLiquidityCumulativeX128).to.eq('442367076997220002502386989661298674892')
      })

      it('can fetch multiple observations', async () => {
        await oracle.initialize({ time: 5, tick: 2, liquidity: BigNumber.from(2).pow(15) })
        await oracle.grow(4)
        await oracle.update({ advanceTimeBy: 13, tick: 6, liquidity: BigNumber.from(2).pow(12) })
        await oracle.advanceTime(5)

        const { tickCumulatives, secondsPerLiquidityCumulativeX128s } = await oracle.observe([0, 3, 8, 13, 15, 18])
        expect(tickCumulatives).to.have.lengthOf(6)
        expect(tickCumulatives[0]).to.eq(56)
        expect(tickCumulatives[1]).to.eq(38)
        expect(tickCumulatives[2]).to.eq(20)
        expect(tickCumulatives[3]).to.eq(10)
        expect(tickCumulatives[4]).to.eq(6)
        expect(tickCumulatives[5]).to.eq(0)
        expect(secondsPerLiquidityCumulativeX128s).to.have.lengthOf(6)
        expect(secondsPerLiquidityCumulativeX128s[0]).to.eq('550383467004691728624232610897330176')
        expect(secondsPerLiquidityCumulativeX128s[1]).to.eq('301153217795020002454768787094765568')
        expect(secondsPerLiquidityCumulativeX128s[2]).to.eq('103845937170696552570609926584401920')
        expect(secondsPerLiquidityCumulativeX128s[3]).to.eq('51922968585348276285304963292200960')
        expect(secondsPerLiquidityCumulativeX128s[4]).to.eq('31153781151208965771182977975320576')
        expect(secondsPerLiquidityCumulativeX128s[5]).to.eq(0)
      })

      it('gas for observe since most recent', async () => {
        await oracle.initialize({ liquidity: 5, tick: -5, time: 5 })
        await oracle.advanceTime(2)
        await snapshotGasCost(oracle.getGasCostOfObserve([1]))
      })

      it('gas for single observation at current time', async () => {
        await oracle.initialize({ liquidity: 5, tick: -5, time: 5 })
        await snapshotGasCost(oracle.getGasCostOfObserve([0]))
      })

      it('gas for single observation at current time counterfactually computed', async () => {
        await oracle.initialize({ liquidity: 5, tick: -5, time: 5 })
        await oracle.advanceTime(5)
        await snapshotGasCost(oracle.getGasCostOfObserve([0]))
      })
    })

    for (const startingTime of [5, 2 ** 32 - 5]) {
      describe(`initialized with 5 observations with starting time of ${startingTime}`, () => {
        const oracleFixture5Observations = async () => {
          const oracle = await oracleFixture()
          await oracle.initialize({ liquidity: 5, tick: -5, time: startingTime })
          await oracle.grow(5)
          await oracle.update({ advanceTimeBy: 3, tick: 1, liquidity: 2 })
          await oracle.update({ advanceTimeBy: 2, tick: -6, liquidity: 4 })
          await oracle.update({ advanceTimeBy: 4, tick: -2, liquidity: 4 })
          await oracle.update({ advanceTimeBy: 1, tick: -2, liquidity: 9 })
          await oracle.update({ advanceTimeBy: 3, tick: 4, liquidity: 2 })
          await oracle.update({ advanceTimeBy: 6, tick: 6, liquidity: 7 })
          return oracle
        }
        let oracle: OracleTest
        beforeEach('set up observations', async () => {
          oracle = await loadFixture(oracleFixture5Observations)
        })

        const observeSingle = async (secondsAgo: number) => {
          const {
            tickCumulatives: [tickCumulative],
            secondsPerLiquidityCumulativeX128s: [secondsPerLiquidityCumulativeX128],
          } = await oracle.observe([secondsAgo])
          return { secondsPerLiquidityCumulativeX128, tickCumulative }
        }

        it('index, cardinality, cardinality next', async () => {
          expect(await oracle.index()).to.eq(1)
          expect(await oracle.cardinality()).to.eq(5)
          expect(await oracle.cardinalityNext()).to.eq(5)
        })
        it('latest observation same time as latest', async () => {
          const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(0)
          expect(tickCumulative).to.eq(-21)
          expect(secondsPerLiquidityCumulativeX128).to.eq('2104079302127802832415199655953100107502')
        })
        it('latest observation 5 seconds after latest', async () => {
          await oracle.advanceTime(5)
          const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(5)
          expect(tickCumulative).to.eq(-21)
          expect(secondsPerLiquidityCumulativeX128).to.eq('2104079302127802832415199655953100107502')
        })
        it('current observation 5 seconds after latest', async () => {
          await oracle.advanceTime(5)
          const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(0)
          expect(tickCumulative).to.eq(9)
          expect(secondsPerLiquidityCumulativeX128).to.eq('2347138135642758877746181518404363115684')
        })
        it('between latest observation and just before latest observation at same time as latest', async () => {
          const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(3)
          expect(tickCumulative).to.eq(-33)
          expect(secondsPerLiquidityCumulativeX128).to.eq('1593655751746395137220137744805447790318')
        })
        it('between latest observation and just before latest observation after the latest observation', async () => {
          await oracle.advanceTime(5)
          const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(8)
          expect(tickCumulative).to.eq(-33)
          expect(secondsPerLiquidityCumulativeX128).to.eq('1593655751746395137220137744805447790318')
        })
        it('older than oldest reverts', async () => {
          await expect(observeSingle(15)).to.be.revertedWith('OLD')
          await oracle.advanceTime(5)
          await expect(observeSingle(20)).to.be.revertedWith('OLD')
        })
        it('oldest observation', async () => {
          const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(14)
          expect(tickCumulative).to.eq(-13)
          expect(secondsPerLiquidityCumulativeX128).to.eq('544451787073501541541399371890829138329')
        })
        it('oldest observation after some time', async () => {
          await oracle.advanceTime(6)
          const { tickCumulative, secondsPerLiquidityCumulativeX128 } = await observeSingle(20)
          expect(tickCumulative).to.eq(-13)
          expect(secondsPerLiquidityCumulativeX128).to.eq('544451787073501541541399371890829138329')
        })

        it('fetch many values', async () => {
          await oracle.advanceTime(6)
          const { tickCumulatives, secondsPerLiquidityCumulativeX128s } = await oracle.observe([
            20,
            17,
            13,
            10,
            5,
            1,
            0,
          ])
          expect({
            tickCumulatives: tickCumulatives.map((tc) => tc.toNumber()),
            secondsPerLiquidityCumulativeX128s: secondsPerLiquidityCumulativeX128s.map((lc) => lc.toString()),
          }).to.matchSnapshot()
        })

        it('gas all of last 20 seconds', async () => {
          await oracle.advanceTime(6)
          await snapshotGasCost(
            oracle.getGasCostOfObserve([20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0])
          )
        })

        it('gas latest equal', async () => {
          await snapshotGasCost(oracle.getGasCostOfObserve([0]))
        })
        it('gas latest transform', async () => {
          await oracle.advanceTime(5)
          await snapshotGasCost(oracle.getGasCostOfObserve([0]))
        })
        it('gas oldest', async () => {
          await snapshotGasCost(oracle.getGasCostOfObserve([14]))
        })
        it('gas between oldest and oldest + 1', async () => {
          await snapshotGasCost(oracle.getGasCostOfObserve([13]))
        })
        it('gas middle', async () => {
          await snapshotGasCost(oracle.getGasCostOfObserve([5]))
        })
      })
    }
  })

  describe.skip('full oracle', function () {
    this.timeout(1_200_000)

    let oracle: OracleTest

    const BATCH_SIZE = 300

    const STARTING_TIME = TEST_POOL_START_TIME

    const maxedOutOracleFixture = async () => {
      const oracle = await oracleFixture()
      await oracle.initialize({ liquidity: 0, tick: 0, time: STARTING_TIME })
      let cardinalityNext = await oracle.cardinalityNext()
      while (cardinalityNext < 65535) {
        const growTo = Math.min(65535, cardinalityNext + BATCH_SIZE)
        console.log('growing from', cardinalityNext, 'to', growTo)
        await oracle.grow(growTo)
        cardinalityNext = growTo
      }

      for (let i = 0; i < 65535; i += BATCH_SIZE) {
        console.log('batch update starting at', i)
        const batch = Array(BATCH_SIZE)
          .fill(null)
          .map((_, j) => ({
            advanceTimeBy: 13,
            tick: -i - j,
            liquidity: i + j,
          }))
        await oracle.batchUpdate(batch)
      }

      return oracle
    }

    beforeEach('create a full oracle', async () => {
      oracle = await loadFixture(maxedOutOracleFixture)
    })

    it('has max cardinality next', async () => {
      expect(await oracle.cardinalityNext()).to.eq(65535)
    })

    it('has max cardinality', async () => {
      expect(await oracle.cardinality()).to.eq(65535)
    })

    it('index wrapped around', async () => {
      expect(await oracle.index()).to.eq(165)
    })

    async function checkObserve(
      secondsAgo: number,
      expected?: { tickCumulative: BigNumberish; secondsPerLiquidityCumulativeX128: BigNumberish }
    ) {
      const { tickCumulatives, secondsPerLiquidityCumulativeX128s } = await oracle.observe([secondsAgo])
      const check = {
        tickCumulative: tickCumulatives[0].toString(),
        secondsPerLiquidityCumulativeX128: secondsPerLiquidityCumulativeX128s[0].toString(),
      }
      if (typeof expected === 'undefined') {
        expect(check).to.matchSnapshot()
      } else {
        expect(check).to.deep.eq({
          tickCumulative: expected.tickCumulative.toString(),
          secondsPerLiquidityCumulativeX128: expected.secondsPerLiquidityCumulativeX128.toString(),
        })
      }
    }

    it('can observe into the ordered portion with exact seconds ago', async () => {
      await checkObserve(100 * 13, {
        secondsPerLiquidityCumulativeX128: '60465049086512033878831623038233202591033',
        tickCumulative: '-27970560813',
      })
    })

    it('can observe into the ordered portion with unexact seconds ago', async () => {
      await checkObserve(100 * 13 + 5, {
        secondsPerLiquidityCumulativeX128: '60465023149565257990964350912969670793706',
        tickCumulative: '-27970232823',
      })
    })

    it('can observe at exactly the latest observation', async () => {
      await checkObserve(0, {
        secondsPerLiquidityCumulativeX128: '60471787506468701386237800669810720099776',
        tickCumulative: '-28055903863',
      })
    })

    it('can observe at exactly the latest observation after some time passes', async () => {
      await oracle.advanceTime(5)
      await checkObserve(5, {
        secondsPerLiquidityCumulativeX128: '60471787506468701386237800669810720099776',
        tickCumulative: '-28055903863',
      })
    })

    it('can observe after the latest observation counterfactual', async () => {
      await oracle.advanceTime(5)
      await checkObserve(3, {
        secondsPerLiquidityCumulativeX128: '60471797865298117996489508104462919730461',
        tickCumulative: '-28056035261',
      })
    })

    it('can observe into the unordered portion of array at exact seconds ago of observation', async () => {
      await checkObserve(200 * 13, {
        secondsPerLiquidityCumulativeX128: '60458300386499273141628780395875293027404',
        tickCumulative: '-27885347763',
      })
    })

    it('can observe into the unordered portion of array at seconds ago between observations', async () => {
      await checkObserve(200 * 13 + 5, {
        secondsPerLiquidityCumulativeX128: '60458274409952896081377821330361274907140',
        tickCumulative: '-27885020273',
      })
    })

    it('can observe the oldest observation 13*65534 seconds ago', async () => {
      await checkObserve(13 * 65534, {
        secondsPerLiquidityCumulativeX128: '33974356747348039873972993881117400879779',
        tickCumulative: '-175890',
      })
    })

    it('can observe the oldest observation 13*65534 + 5 seconds ago if time has elapsed', async () => {
      await oracle.advanceTime(5)
      await checkObserve(13 * 65534 + 5, {
        secondsPerLiquidityCumulativeX128: '33974356747348039873972993881117400879779',
        tickCumulative: '-175890',
      })
    })

    it('gas cost of observe(0)', async () => {
      await snapshotGasCost(oracle.getGasCostOfObserve([0]))
    })
    it('gas cost of observe(200 * 13)', async () => {
      await snapshotGasCost(oracle.getGasCostOfObserve([200 + 13]))
    })
    it('gas cost of observe(200 * 13 + 5)', async () => {
      await snapshotGasCost(oracle.getGasCostOfObserve([200 + 13 + 5]))
    })
    it('gas cost of observe(0) after 5 seconds', async () => {
      await oracle.advanceTime(5)
      await snapshotGasCost(oracle.getGasCostOfObserve([0]))
    })
    it('gas cost of observe(5) after 5 seconds', async () => {
      await oracle.advanceTime(5)
      await snapshotGasCost(oracle.getGasCostOfObserve([5]))
    })
    it('gas cost of observe(oldest)', async () => {
      await snapshotGasCost(oracle.getGasCostOfObserve([65534 * 13]))
    })
    it('gas cost of observe(oldest) after 5 seconds', async () => {
      await oracle.advanceTime(5)
      await snapshotGasCost(oracle.getGasCostOfObserve([65534 * 13 + 5]))
    })
  })
})
