import { assert, beforeAll, beforeEach, describe, it, vi, expect } from 'vitest'
import Errors from '../../../../app/src/Features/Errors/Errors.js'
import tk from 'timekeeper'
import { RequestFailedError } from '@overleaf/fetch-utils'

const modulePath = '../../../../app/src/Features/Docstore/DocstoreManager'

vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
  vi.importActual('../../../../app/src/Features/Errors/Errors.js')
)

describe('DocstoreManager', function () {
  let DocstoreManager, FetchUtils, projectId, docId, settings

  beforeEach(async function () {
    settings = {
      apis: {
        docstore: {
          url: 'http://docstore.overleaf.com',
        },
      },
    }

    vi.doMock('@overleaf/settings', () => ({
      default: settings,
    }))

    FetchUtils = {
      fetchNothing: vi.fn().mockResolvedValue(),
      fetchJson: vi.fn().mockResolvedValue({}),
      RequestFailedError,
    }

    vi.doMock('@overleaf/fetch-utils', () => FetchUtils)

    DocstoreManager = (await import(modulePath)).default

    projectId = 'project-id-123'
    docId = 'doc-id-123'
  })

  describe('deleteDoc', function () {
    describe('with a successful response code', function () {
      // for assertions on the deletedAt timestamp, we need to freeze the clock.
      beforeAll(function () {
        tk.freeze(Date.now())
      })
      afterAll(function () {
        tk.reset()
      })

      beforeEach(async function () {
        await DocstoreManager.promises.deleteDoc(
          projectId,
          docId,
          'wombat.tex',
          new Date()
        )
      })

      it('should delete the doc in the docstore api', function () {
        const url = new URL(settings.apis.docstore.url)
        url.pathname = `/project/${projectId}/doc/${docId}`
        expect(FetchUtils.fetchNothing).toHaveBeenCalledWith(url, {
          json: { deleted: true, deletedAt: new Date(), name: 'wombat.tex' },
          signal: expect.anything(),
          method: 'PATCH',
        })
      })
    })

    describe('with a failed response code', function () {
      beforeEach(function () {
        FetchUtils.fetchNothing.mockImplementation((url, opts) => {
          throw new RequestFailedError(url, opts, { status: 500 })
        })
      })

      it('should reject with an error', async function () {
        let error

        try {
          await DocstoreManager.promises.deleteDoc(
            projectId,
            docId,
            'main.tex',
            new Date()
          )
        } catch (err) {
          error = err
        }

        expect(error).to.be.instanceOf(Error)
        expect(error).to.have.property(
          'message',
          'docstore api responded with non-success code'
        )
      })
    })

    describe('with a missing (404) response code', function () {
      beforeEach(function () {
        FetchUtils.fetchNothing.mockImplementation((url, opts) => {
          throw new RequestFailedError(url, opts, { status: 404 })
        })
      })

      it('should reject with an error', async function () {
        let error
        try {
          await DocstoreManager.promises.deleteDoc(
            projectId,
            docId,
            'main.tex',
            new Date()
          )
        } catch (err) {
          error = err
        }

        expect(error).to.be.instanceOf(Error)
        expect(error).to.have.property(
          'message',
          'tried to delete doc not in docstore'
        )
      })
    })
  })

  describe('updateDoc', function () {
    let lines, modified, ranges, rev, updateDocResponse, version
    beforeEach(function () {
      lines = ['mock', 'doc', 'lines']
      rev = 5
      version = 42
      ranges = { mock: 'ranges' }
      modified = true
    })

    describe('with a successful response code', async function () {
      beforeEach(async function () {
        FetchUtils.fetchJson.mockResolvedValue({
          modified,
          rev,
        })
        updateDocResponse = await DocstoreManager.promises.updateDoc(
          projectId,
          docId,
          lines,
          version,
          ranges
        )
      })

      it('should update the doc in the docstore api', function () {
        expect(FetchUtils.fetchJson).toHaveBeenCalledWith(
          new URL(
            `${settings.apis.docstore.url}/project/${projectId}/doc/${docId}`
          ),
          {
            signal: expect.anything(),
            method: 'POST',
            json: {
              lines,
              version,
              ranges,
            },
          }
        )
      })

      it('should return the modified status and revision', function () {
        expect(updateDocResponse).to.haveOwnProperty('modified', modified)
        expect(updateDocResponse).to.haveOwnProperty('rev', rev)
      })
    })

    describe('with a failed response code', function () {
      beforeEach(function () {
        FetchUtils.fetchJson.mockImplementation((url, opts) => {
          throw new RequestFailedError(url, opts, { status: 500 })
        })
      })

      it('should reject with an error', async function () {
        let error

        try {
          await DocstoreManager.promises.updateDoc(
            projectId,
            docId,
            lines,
            version,
            ranges
          )
          assert.fail('updateDoc should have thrown an error')
        } catch (err) {
          error = err
        }

        expect(error).to.be.instanceOf(Error)
        expect(error).to.have.property(
          'message',
          'docstore api responded with non-success code'
        )
      })
    })
  })

  describe('getDoc', function () {
    let doc, getDocResponse, lines, ranges, rev, version
    beforeEach(function () {
      lines = ['mock', 'doc', 'lines']
      rev = 5
      version = 42
      ranges = { mock: 'ranges' }
      doc = {
        lines,
        rev,
        version,
        ranges,
      }
    })

    describe('with a successful response code', function () {
      beforeEach(async function () {
        FetchUtils.fetchJson.mockResolvedValue(doc)
        getDocResponse = await DocstoreManager.promises.getDoc(projectId, docId)
      })

      it('should get the doc from the docstore api', function () {
        expect(FetchUtils.fetchJson).toHaveBeenCalledWith(
          new URL(
            `${settings.apis.docstore.url}/project/${projectId}/doc/${docId}`
          ),
          {
            signal: expect.anything(),
          }
        )
      })

      it('should resolve with the lines, version and rev', function () {
        expect(getDocResponse).to.eql({
          lines,
          rev,
          version,
          ranges,
        })
      })
    })

    describe('with a failed response code', function () {
      beforeEach(function () {
        FetchUtils.fetchJson.mockImplementation((url, opts) => {
          throw new RequestFailedError(url, opts, { status: 500 })
        })
      })

      it('should reject with an error', async function () {
        let error

        try {
          await DocstoreManager.promises.getDoc(projectId, docId)
          assert.fail('getDoc should have thrown an error')
        } catch (err) {
          error = err
        }

        expect(error).to.be.instanceOf(Error)
        expect(error).to.have.property(
          'message',
          'docstore api responded with non-success code'
        )
      })
    })

    describe('with include_deleted=true', function () {
      beforeEach(async function () {
        FetchUtils.fetchJson.mockResolvedValue(doc)
        getDocResponse = await DocstoreManager.promises.getDoc(
          projectId,
          docId,
          { include_deleted: true }
        )
      })

      it('should get the doc from the docstore api (including deleted)', function () {
        expect(FetchUtils.fetchJson).toHaveBeenCalledWith(
          new URL(
            `${settings.apis.docstore.url}/project/${projectId}/doc/${docId}?include_deleted=true`
          ),
          {
            signal: expect.anything(),
          }
        )
      })

      it('should resolve with the lines, version and rev', function () {
        expect(getDocResponse).to.eql({
          lines,
          rev,
          version,
          ranges,
        })
      })
    })

    describe('with peek=true', function () {
      beforeEach(async function () {
        await DocstoreManager.promises.getDoc(projectId, docId, {
          peek: true,
        })
      })

      it('should call the docstore peek url', function () {
        expect(FetchUtils.fetchJson).toHaveBeenCalledWith(
          new URL(
            `${settings.apis.docstore.url}/project/${projectId}/doc/${docId}/peek`
          ),
          {
            signal: expect.anything(),
          }
        )
      })
    })

    describe('with a missing (404) response code', function () {
      beforeEach(function () {
        FetchUtils.fetchJson.mockImplementation((url, opts) => {
          throw new RequestFailedError(url, opts, { status: 404 })
        })
      })

      it('should reject with an error', async function () {
        let error

        try {
          await DocstoreManager.promises.getDoc(projectId, docId)
          assert.fail('getDoc should have thrown an error')
        } catch (err) {
          error = err
        }

        expect(error).to.be.instanceOf(Errors.NotFoundError)
        expect(error).to.have.property('message', 'doc not found in docstore')
      })
    })
  })

  describe('getAllDocs', function () {
    describe('with a successful response code', function () {
      let getAllDocsResult
      let docs
      beforeEach(async function () {
        docs = [{ _id: 'mock-doc-id' }]
        FetchUtils.fetchJson.mockResolvedValue(docs)
        getAllDocsResult = await DocstoreManager.promises.getAllDocs(projectId)
      })

      it('should get all the project docs in the docstore api', function () {
        expect(FetchUtils.fetchJson).toBeCalledWith(
          new URL(`${settings.apis.docstore.url}/project/${projectId}/doc`),
          {
            signal: expect.anything(),
          }
        )
      })

      it('should return the docs', function () {
        expect(getAllDocsResult).to.eql(docs)
      })
    })

    describe('with a failed response code', function () {
      beforeEach(function () {
        FetchUtils.fetchJson.mockImplementation((url, opts) => {
          throw new RequestFailedError(url, opts, { status: 500 })
        })
      })

      it('should reject with an error', async function () {
        let error

        try {
          await DocstoreManager.promises.getAllDocs(projectId)
          assert.fail('getAllDocs should have thrown an error')
        } catch (err) {
          error = err
        }

        expect(error).to.be.instanceOf(Error)
        expect(error).to.have.property(
          'message',
          'docstore api responded with non-success code'
        )
      })
    })
  })

  describe('getAllDeletedDocs', function () {
    describe('with a successful response code', function () {
      let getAllDeletedDocsResponse
      let docs
      beforeEach(async function () {
        docs = [{ _id: 'mock-doc-id', name: 'foo.tex' }]
        FetchUtils.fetchJson.mockResolvedValue(docs)
        getAllDeletedDocsResponse =
          await DocstoreManager.promises.getAllDeletedDocs(projectId)
      })

      it('should get all the project docs in the docstore api', function () {
        expect(FetchUtils.fetchJson).toHaveBeenCalledWith(
          new URL(
            `${settings.apis.docstore.url}/project/${projectId}/doc-deleted`
          ),
          {
            signal: expect.anything(),
          }
        )
      })

      it('should resolve with the docs', function () {
        expect(getAllDeletedDocsResponse).to.eql(docs)
      })
    })

    describe('with an error', function () {
      beforeEach(async function () {
        FetchUtils.fetchJson.mockRejectedValue(new Error('connect failed'))
      })

      it('should reject with an error', async function () {
        let error

        try {
          await DocstoreManager.promises.getAllDeletedDocs(projectId)
          assert.fail('getAllDeletedDocs should have thrown an error')
        } catch (err) {
          error = err
        }

        expect(error).to.be.instanceOf(Error)
        expect(error).to.have.property('message', 'connect failed')
      })
    })

    describe('with a failed response code', function () {
      beforeEach(function () {
        FetchUtils.fetchJson.mockImplementation((url, opts) => {
          throw new RequestFailedError(url, opts, { status: 500 })
        })
      })

      it('should reject with an error', async function () {
        let error

        try {
          await DocstoreManager.promises.getAllDeletedDocs(projectId)
          assert.fail('getAllDeletedDocs should have thrown an error')
        } catch (err) {
          error = err
        }

        expect(error).to.be.instanceOf(Error)
        expect(error).to.have.property(
          'message',
          'docstore api responded with non-success code'
        )
      })
    })
  })

  describe('getAllRanges', function () {
    describe('with a successful response code', function () {
      let getAllRangesResult
      let docs
      beforeEach(async function () {
        docs = [{ _id: 'mock-doc-id', ranges: 'mock-ranges' }]
        FetchUtils.fetchJson.mockResolvedValue(docs)
        getAllRangesResult =
          await DocstoreManager.promises.getAllRanges(projectId)
      })

      it('should get all the project doc ranges in the docstore api', function () {
        expect(FetchUtils.fetchJson).toHaveBeenCalledWith(
          new URL(`${settings.apis.docstore.url}/project/${projectId}/ranges`),
          {
            signal: expect.anything(),
          }
        )
      })

      it('should return the docs', async function () {
        expect(getAllRangesResult).to.eql(docs)
      })
    })

    describe('with a failed response code', function () {
      beforeEach(function () {
        FetchUtils.fetchJson.mockImplementation((url, opts) => {
          throw new RequestFailedError(url, opts, { status: 500 })
        })
      })

      it('should reject with an error', async function () {
        let error

        try {
          await DocstoreManager.promises.getAllRanges(projectId)
          assert.fail('getAllRanges should have thrown an error')
        } catch (err) {
          error = err
        }

        expect(error).to.be.instanceOf(Error)
        expect(error).to.have.property(
          'message',
          'docstore api responded with non-success code'
        )
      })
    })
  })

  describe('archiveProject', function () {
    describe('with a successful response code', function () {
      it('should resolve', async function () {
        await expect(DocstoreManager.promises.archiveProject(projectId)).to
          .eventually.be.fulfilled
      })
    })

    describe('with a failed response code', function () {
      beforeEach(function () {
        FetchUtils.fetchNothing.mockImplementation((url, opts) => {
          throw new RequestFailedError(url, opts, { status: 500 })
        })
      })

      it('should reject with an error', async function () {
        let error

        try {
          await DocstoreManager.promises.archiveProject(projectId)
          assert.fail('archiveProject should have thrown an error')
        } catch (err) {
          error = err
        }

        expect(error).to.be.instanceOf(Error)
        expect(error).to.have.property(
          'message',
          'docstore api responded with non-success code'
        )
      })
    })
  })

  describe('unarchiveProject', function () {
    describe('with a successful response code', function () {
      it('should resolve', async function () {
        await expect(DocstoreManager.promises.unarchiveProject(projectId)).to
          .eventually.be.fulfilled
      })
    })

    describe('with a failed response code', function () {
      beforeEach(function () {
        FetchUtils.fetchNothing.mockImplementation((url, opts) => {
          throw new RequestFailedError(url, opts, { status: 500 })
        })
      })

      it('should reject with an error', async function () {
        let error

        try {
          await DocstoreManager.promises.unarchiveProject(projectId)
          assert.fail('unarchiveProject should have thrown an error')
        } catch (err) {
          error = err
        }

        expect(error).to.be.instanceOf(Error)
        expect(error).to.have.property(
          'message',
          'docstore api responded with non-success code'
        )
      })
    })
  })

  describe('destroyProject', function () {
    describe('with a successful response code', function () {
      it('should resolve', async function () {
        await expect(DocstoreManager.promises.destroyProject(projectId)).to
          .eventually.be.fulfilled
      })
    })

    describe('with a failed response code', function () {
      beforeEach(function () {
        FetchUtils.fetchNothing.mockImplementation((url, opts) => {
          throw new RequestFailedError(url, opts, { status: 500 })
        })
      })

      it('should reject with an error', async function () {
        let error

        try {
          await DocstoreManager.promises.destroyProject(projectId)
          assert.fail('destroyProject should have thrown an error')
        } catch (err) {
          error = err
        }

        expect(error).to.be.instanceOf(Error)
        expect(error).to.have.property(
          'message',
          'docstore api responded with non-success code'
        )
      })
    })
  })
})
