import { streamToBuffer } from '@atproto/common'
import * as crypto from '@atproto/crypto'
import {
  CidSet,
  Repo,
  RepoContents,
  RepoVerificationError,
  getAndParseRecord,
  readCar,
  readCarWithRoot,
} from '../src'
import { MemoryBlockstore } from '../src/storage'
import * as sync from '../src/sync'
import * as util from './_util'

describe('Repo Sync', () => {
  let storage: MemoryBlockstore
  let repo: Repo
  let keypair: crypto.Keypair
  let repoData: RepoContents

  const repoDid = 'did:example:test'

  beforeAll(async () => {
    storage = new MemoryBlockstore()
    keypair = await crypto.Secp256k1Keypair.create()
    repo = await Repo.create(storage, repoDid, keypair)
    const filled = await util.fillRepo(repo, keypair, 20)
    repo = filled.repo
    repoData = filled.data
  })

  it('sync a full repo', async () => {
    const carBytes = await streamToBuffer(sync.getFullRepo(storage, repo.cid))
    const car = await readCarWithRoot(carBytes)
    const verified = await sync.verifyRepo(
      car.blocks,
      car.root,
      repoDid,
      keypair.did(),
    )
    const syncStorage = new MemoryBlockstore()
    await syncStorage.applyCommit(verified.commit)
    const loadedRepo = await Repo.load(syncStorage, car.root)
    const contents = await loadedRepo.getContents()
    expect(contents).toEqual(repoData)
    const contentsFromOps: RepoContents = {}
    for (const write of verified.creates) {
      contentsFromOps[write.collection] ??= {}
      const parsed = await getAndParseRecord(car.blocks, write.cid)
      contentsFromOps[write.collection][write.rkey] = parsed.record
    }
    expect(contentsFromOps).toEqual(repoData)
  })

  it('does not sync duplicate blocks', async () => {
    const carBytes = await streamToBuffer(sync.getFullRepo(storage, repo.cid))
    const car = await readCar(carBytes)
    const cids = new CidSet()
    car.blocks.forEach((_, cid) => {
      if (cids.has(cid)) {
        throw new Error(`duplicate block: :${cid.toString()}`)
      }
      cids.add(cid)
    })
  })

  it('syncs a repo that is behind', async () => {
    // add more to providers's repo & have consumer catch up
    const edit = await util.formatEdit(repo, repoData, keypair, {
      adds: 10,
      updates: 10,
      deletes: 10,
    })
    const verified = await sync.verifyDiff(
      repo,
      edit.commit.newBlocks,
      edit.commit.cid,
      repoDid,
      keypair.did(),
    )
    await storage.applyCommit(verified.commit)
    repo = await Repo.load(storage, verified.commit.cid)
    const contents = await repo.getContents()
    expect(contents).toEqual(edit.data)
  })

  it('throws on a bad signature', async () => {
    const badRepo = await util.addBadCommit(repo, keypair)
    const carBytes = await streamToBuffer(
      sync.getFullRepo(storage, badRepo.cid),
    )
    const car = await readCarWithRoot(carBytes)
    await expect(
      sync.verifyRepo(car.blocks, car.root, repoDid, keypair.did()),
    ).rejects.toThrow(RepoVerificationError)
  })
})
