import { vi, expect } from 'vitest'
import mongodb from 'mongodb-legacy'
import sinon from 'sinon'
const modulePath =
  '../../../../app/src/Features/Project/ProjectRootDocManager.mjs'

const { ObjectId } = mongodb

describe('ProjectRootDocManager', function () {
  beforeEach(async function (ctx) {
    ctx.project_id = 'project-123'
    ctx.docPaths = {}
    ctx.docId1 = new ObjectId()
    ctx.docId2 = new ObjectId()
    ctx.docId3 = new ObjectId()
    ctx.docId4 = new ObjectId()
    ctx.docPaths[ctx.docId1] = '/chapter1.tex'
    ctx.docPaths[ctx.docId2] = '/main.tex'
    ctx.docPaths[ctx.docId3] = '/nested/chapter1a.tex'
    ctx.docPaths[ctx.docId4] = '/nested/chapter1b.tex'
    ctx.sl_req_id = 'sl-req-id-123'
    ctx.globbyFiles = ['a.tex', 'b.tex', 'main.tex']
    ctx.globby = sinon.stub().resolves(ctx.globbyFiles)

    ctx.fs = {
      promises: {
        readFile: sinon.stub().rejects(new Error('file not found')),
        stat: sinon.stub().resolves({ size: 100 }),
      },
    }
    ctx.ProjectEntityHandler = {
      promises: {},
    }
    ctx.ProjectEntityUpdateHandler = {
      promises: {},
    }
    ctx.ProjectGetter = {
      promises: {},
    }

    vi.doMock(
      '../../../../app/src/Features/Project/ProjectEntityHandler',
      () => ({
        default: ctx.ProjectEntityHandler,
      })
    )

    vi.doMock(
      '../../../../app/src/Features/Project/ProjectEntityUpdateHandler',
      () => ({
        default: ctx.ProjectEntityUpdateHandler,
      })
    )

    vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
      default: ctx.ProjectGetter,
    }))

    vi.doMock('../../../../app/src/infrastructure/GracefulShutdown', () => ({
      BackgroundTaskTracker: class {
        add() {}
        done() {}
      },
    }))

    vi.doMock('globby', () => ({
      default: ctx.globby,
    }))

    vi.doMock('fs', () => ({
      default: ctx.fs,
    }))

    ctx.ProjectRootDocManager = (await import(modulePath)).default
  })

  describe('setRootDocAutomatically', function () {
    beforeEach(function (ctx) {
      ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon
        .stub()
        .resolves()
      ctx.ProjectEntityUpdateHandler.isPathValidForRootDoc = sinon
        .stub()
        .returns(true)
    })
    describe('when there is a suitable root doc', function () {
      beforeEach(async function (ctx) {
        ctx.docs = {
          '/chapter1.tex': {
            _id: ctx.docId1,
            lines: [
              'something else',
              '\\begin{document}',
              'Hello world',
              '\\end{document}',
            ],
          },
          '/main.tex': {
            _id: ctx.docId2,
            lines: [
              'different line',
              '\\documentclass{article}',
              '\\input{chapter1}',
            ],
          },
          '/nested/chapter1a.tex': {
            _id: ctx.docId3,
            lines: ['Hello world'],
          },
          '/nested/chapter1b.tex': {
            _id: ctx.docId4,
            lines: ['Hello world'],
          },
        }
        ctx.ProjectEntityHandler.promises.getAllDocs = sinon
          .stub()
          .resolves(ctx.docs)
        await ctx.ProjectRootDocManager.promises.setRootDocAutomatically(
          ctx.project_id
        )
      })

      it('should check the docs of the project', function (ctx) {
        ctx.ProjectEntityHandler.promises.getAllDocs
          .calledWith(ctx.project_id)
          .should.equal(true)
      })

      it('should set the root doc to the doc containing a documentclass', function (ctx) {
        ctx.ProjectEntityUpdateHandler.promises.setRootDoc
          .calledWith(ctx.project_id, ctx.docId2)
          .should.equal(true)
      })
    })

    describe('when the root doc is an Rtex file', function () {
      beforeEach(async function (ctx) {
        ctx.docs = {
          '/chapter1.tex': {
            _id: ctx.docId1,
            lines: ['\\begin{document}', 'Hello world', '\\end{document}'],
          },
          '/main.Rtex': {
            _id: ctx.docId2,
            lines: ['\\documentclass{article}', '\\input{chapter1}'],
          },
        }
        ctx.ProjectEntityHandler.promises.getAllDocs = sinon
          .stub()
          .resolves(ctx.docs)
        await ctx.ProjectRootDocManager.promises.setRootDocAutomatically(
          ctx.project_id
        )
      })

      it('should set the root doc to the doc containing a documentclass', function (ctx) {
        ctx.ProjectEntityUpdateHandler.promises.setRootDoc
          .calledWith(ctx.project_id, ctx.docId2)
          .should.equal(true)
      })
    })

    describe('when there is no suitable root doc', function () {
      beforeEach(async function (ctx) {
        ctx.docs = {
          '/chapter1.tex': {
            _id: ctx.docId1,
            lines: ['\\begin{document}', 'Hello world', '\\end{document}'],
          },
          '/style.bst': {
            _id: ctx.docId2,
            lines: ['%Example: \\documentclass{article}'],
          },
        }
        ctx.ProjectEntityHandler.promises.getAllDocs = sinon
          .stub()
          .resolves(ctx.docs)
        await ctx.ProjectRootDocManager.promises.setRootDocAutomatically(
          ctx.project_id
        )
      })

      it('should not set the root doc to the doc containing a documentclass', function (ctx) {
        ctx.ProjectEntityUpdateHandler.promises.setRootDoc.called.should.equal(
          false
        )
      })
    })
  })

  describe('findRootDocFileFromDirectory', function () {
    beforeEach(function (ctx) {
      ctx.fs.promises.readFile.withArgs('/foo/a.tex').resolves('Hello World!')
      ctx.fs.promises.readFile
        .withArgs('/foo/b.tex')
        .resolves("I'm a little teapot, get me out of here.")
      ctx.fs.promises.readFile
        .withArgs('/foo/main.tex')
        .resolves("Help, I'm trapped in a unit testing factory")
      ctx.fs.promises.readFile
        .withArgs('/foo/c.tex')
        .resolves('Tomato, tomahto.')
      ctx.fs.promises.readFile
        .withArgs('/foo/a/a.tex')
        .resolves('Potato? Potahto. Potootee!')
      ctx.documentclassContent = '% test\n\\documentclass\n% test'
    })

    describe('when there is a file in a subfolder', function () {
      beforeEach(function (ctx) {
        // have to splice globbyFiles weirdly because of the way the stubbed globby method handles references
        ctx.globbyFiles.splice(
          0,
          ctx.globbyFiles.length,
          'c.tex',
          'a.tex',
          'a/a.tex',
          'b.tex'
        )
      })

      it('processes the root folder files first, and then the subfolder, in alphabetical order', async function (ctx) {
        const { path } =
          await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
            '/foo'
          )
        expect(path).to.equal('a.tex')
        sinon.assert.callOrder(
          ctx.fs.promises.readFile.withArgs('/foo/a.tex'),
          ctx.fs.promises.readFile.withArgs('/foo/b.tex'),
          ctx.fs.promises.readFile.withArgs('/foo/c.tex'),
          ctx.fs.promises.readFile.withArgs('/foo/a/a.tex')
        )
      })

      it('processes smaller files first', async function (ctx) {
        ctx.fs.promises.stat.withArgs('/foo/c.tex').resolves({ size: 1 })
        const { path } =
          await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
            '/foo'
          )
        expect(path).to.equal('c.tex')
        sinon.assert.callOrder(
          ctx.fs.promises.readFile.withArgs('/foo/c.tex'),
          ctx.fs.promises.readFile.withArgs('/foo/a.tex'),
          ctx.fs.promises.readFile.withArgs('/foo/b.tex'),
          ctx.fs.promises.readFile.withArgs('/foo/a/a.tex')
        )
      })
    })

    describe('when main.tex contains a documentclass', function () {
      beforeEach(function (ctx) {
        ctx.fs.promises.readFile
          .withArgs('/foo/main.tex')
          .resolves(ctx.documentclassContent)
      })

      it('returns main.tex', async function (ctx) {
        const { path, content } =
          await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
            '/foo'
          )
        expect(path).to.equal('main.tex')
        expect(content).to.equal(ctx.documentclassContent)
      })

      it('processes main.text first and stops processing when it finds the content', async function (ctx) {
        await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
          '/foo'
        )
        expect(ctx.fs.promises.readFile).to.be.calledWith('/foo/main.tex')
        expect(ctx.fs.promises.readFile).not.to.be.calledWith('/foo/a.tex')
      })
    })

    describe('when main.tex does not contain a line starting with \\documentclass', function () {
      beforeEach(function (ctx) {
        ctx.fs.promises.readFile.withArgs('/foo/a.tex').resolves('foo')
        ctx.fs.promises.readFile.withArgs('/foo/main.tex').resolves('foo')
        ctx.fs.promises.readFile.withArgs('/foo/z.tex').resolves('foo')
        ctx.fs.promises.readFile
          .withArgs('/foo/nested/chapter1a.tex')
          .resolves('foo')
      })

      it('returns the first .tex file from the root folder', async function (ctx) {
        ctx.globbyFiles.splice(
          0,
          ctx.globbyFiles.length,
          'a.tex',
          'z.tex',
          'nested/chapter1a.tex'
        )

        const { path, content } =
          await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
            '/foo'
          )
        expect(path).to.equal('a.tex')
        expect(content).to.equal('foo')
      })

      it('returns main.tex file from the root folder', async function (ctx) {
        ctx.globbyFiles.splice(
          0,
          ctx.globbyFiles.length,
          'a.tex',
          'z.tex',
          'main.tex',
          'nested/chapter1a.tex'
        )

        const { path, content } =
          await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
            '/foo'
          )
        expect(path).to.equal('main.tex')
        expect(content).to.equal('foo')
      })
    })

    describe('when a.tex contains a documentclass', function () {
      beforeEach(function (ctx) {
        ctx.fs.promises.readFile
          .withArgs('/foo/a.tex')
          .resolves(ctx.documentclassContent)
      })

      it('returns a.tex', async function (ctx) {
        const { path, content } =
          await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
            '/foo'
          )
        expect(path).to.equal('a.tex')
        expect(content).to.equal(ctx.documentclassContent)
      })

      it('processes main.text first and stops processing when it finds the content', async function (ctx) {
        await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
          '/foo'
        )
        expect(ctx.fs.promises.readFile).to.be.calledWith('/foo/main.tex')
        expect(ctx.fs.promises.readFile).to.be.calledWith('/foo/a.tex')
        expect(ctx.fs.promises.readFile).not.to.be.calledWith('/foo/b.tex')
      })
    })

    describe('when there is no documentclass', function () {
      it('returns with no error', async function (ctx) {
        await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
          '/foo'
        )
      })

      it('processes all the files', async function (ctx) {
        await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
          '/foo'
        )
        expect(ctx.fs.promises.readFile).to.be.calledWith('/foo/main.tex')
        expect(ctx.fs.promises.readFile).to.be.calledWith('/foo/a.tex')
        expect(ctx.fs.promises.readFile).to.be.calledWith('/foo/b.tex')
      })
    })

    describe('when there is an error reading a file', function () {
      beforeEach(function (ctx) {
        ctx.fs.promises.readFile
          .withArgs('/foo/a.tex')
          .rejects(new Error('something went wrong'))
      })

      it('returns an error', async function (ctx) {
        let error

        try {
          await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
            '/foo'
          )
        } catch (err) {
          error = err
        }

        expect(error).to.exist
      })
    })
  })

  describe('setRootDocFromName', function () {
    describe('when there is a suitable root doc', function () {
      beforeEach(async function (ctx) {
        ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById = sinon
          .stub()
          .resolves(ctx.docPaths)
        ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon
          .stub()
          .resolves()
        await ctx.ProjectRootDocManager.promises.setRootDocFromName(
          ctx.project_id,
          '/main.tex'
        )
      })

      it('should check the docs of the project', function (ctx) {
        ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById
          .calledWith(ctx.project_id)
          .should.equal(true)
      })

      it('should set the root doc to main.tex', function (ctx) {
        ctx.ProjectEntityUpdateHandler.promises.setRootDoc
          .calledWith(ctx.project_id, ctx.docId2.toString())
          .should.equal(true)
      })
    })

    describe('when there is a suitable root doc but the leading slash is missing', function () {
      beforeEach(async function (ctx) {
        ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById = sinon
          .stub()
          .resolves(ctx.docPaths)
        ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon
          .stub()
          .resolves()
        await ctx.ProjectRootDocManager.promises.setRootDocFromName(
          ctx.project_id,
          'main.tex'
        )
      })

      it('should check the docs of the project', function (ctx) {
        ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById
          .calledWith(ctx.project_id)
          .should.equal(true)
      })

      it('should set the root doc to main.tex', function (ctx) {
        ctx.ProjectEntityUpdateHandler.promises.setRootDoc
          .calledWith(ctx.project_id, ctx.docId2.toString())
          .should.equal(true)
      })
    })

    describe('when there is a suitable root doc with a basename match', function () {
      beforeEach(async function (ctx) {
        ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById = sinon
          .stub()
          .resolves(ctx.docPaths)
        ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon
          .stub()
          .resolves()
        await ctx.ProjectRootDocManager.promises.setRootDocFromName(
          ctx.project_id,
          'chapter1a.tex'
        )
      })

      it('should check the docs of the project', function (ctx) {
        ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById
          .calledWith(ctx.project_id)
          .should.equal(true)
      })

      it('should set the root doc using the basename', function (ctx) {
        ctx.ProjectEntityUpdateHandler.promises.setRootDoc
          .calledWith(ctx.project_id, ctx.docId3.toString())
          .should.equal(true)
      })
    })

    describe('when there is a suitable root doc but the filename is in quotes', function () {
      beforeEach(async function (ctx) {
        ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById = sinon
          .stub()
          .resolves(ctx.docPaths)
        ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon
          .stub()
          .resolves()
        await ctx.ProjectRootDocManager.promises.setRootDocFromName(
          ctx.project_id,
          "'main.tex'"
        )
      })

      it('should check the docs of the project', function (ctx) {
        ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById
          .calledWith(ctx.project_id)
          .should.equal(true)
      })

      it('should set the root doc to main.tex', function (ctx) {
        ctx.ProjectEntityUpdateHandler.promises.setRootDoc
          .calledWith(ctx.project_id, ctx.docId2.toString())
          .should.equal(true)
      })
    })

    describe('when there is no suitable root doc', function () {
      beforeEach(async function (ctx) {
        ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById = sinon
          .stub()
          .resolves(ctx.docPaths)
        ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon
          .stub()
          .resolves()
        await ctx.ProjectRootDocManager.promises.setRootDocFromName(
          ctx.project_id,
          'other.tex'
        )
      })

      it('should not set the root doc', function (ctx) {
        ctx.ProjectEntityUpdateHandler.promises.setRootDoc.called.should.equal(
          false
        )
      })
    })
  })

  describe('ensureRootDocumentIsValid', function () {
    beforeEach(function (ctx) {
      ctx.project = {}
      ctx.ProjectGetter.promises.getProject = sinon.stub().resolves(ctx.project)
      ctx.ProjectGetter.promises.getProject = sinon.stub().resolves(ctx.project)
      ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon
        .stub()
        .resolves()
      ctx.ProjectEntityUpdateHandler.promises.unsetRootDoc = sinon
        .stub()
        .resolves()
      ctx.ProjectRootDocManager.promises.setRootDocAutomatically = sinon
        .stub()
        .resolves(null)
    })

    describe('when the root doc is set', function () {
      describe('when the root doc is valid', function () {
        let result
        beforeEach(async function (ctx) {
          ctx.project.rootDoc_id = ctx.docId2
          ctx.ProjectEntityHandler.promises.getDocPathFromProjectByDocId = sinon
            .stub()
            .resolves(ctx.docPaths[ctx.docId2])
          result =
            await ctx.ProjectRootDocManager.promises.ensureRootDocumentIsValid(
              ctx.project_id
            )
        })

        it('should return the path and id', ctx => {
          result.should.deep.equal({
            rootDocId: ctx.docId2,
            rootResourcePath: ctx.docPaths[ctx.docId2],
          })
        })

        it('should find the project without doc lines', function (ctx) {
          ctx.ProjectGetter.promises.getProject
            .calledWith(ctx.project_id)
            .should.equal(true)
        })

        it('should not try to update the project rootDoc_id', function (ctx) {
          ctx.ProjectRootDocManager.promises.setRootDocAutomatically.called.should.equal(
            false
          )
        })
      })

      describe('when the root doc is not valid', function () {
        let result
        beforeEach(async function (ctx) {
          ctx.project.rootDoc_id = new ObjectId()
          ctx.ProjectEntityHandler.promises.getDocPathFromProjectByDocId = sinon
            .stub()
            .resolves(null)
          ctx.automaticRootDoc = {
            rootDocId: ctx.docId4,
            rootResourcePath: ctx.docPaths[ctx.docId4],
          }
          ctx.ProjectRootDocManager.promises.setRootDocAutomatically.resolves(
            ctx.automaticRootDoc
          )
          result =
            await ctx.ProjectRootDocManager.promises.ensureRootDocumentIsValid(
              ctx.project_id
            )
        })

        it('should return the path and id', ctx => {
          result.should.deep.equal(ctx.automaticRootDoc)
        })

        it('should find the project without doc lines', function (ctx) {
          ctx.ProjectGetter.promises.getProject
            .calledWith(ctx.project_id)
            .should.equal(true)
        })

        it('should unset the root doc', function (ctx) {
          ctx.ProjectEntityUpdateHandler.promises.unsetRootDoc
            .calledWith(ctx.project_id)
            .should.equal(true)
        })

        it('should try to find a new rootDoc', function (ctx) {
          ctx.ProjectRootDocManager.promises.setRootDocAutomatically.called.should.equal(
            true
          )
        })
      })
    })

    describe('when the root doc is not set', function () {
      let result
      beforeEach(async function (ctx) {
        ctx.automaticRootDoc = {
          rootDocId: ctx.docId4,
          rootResourcePath: ctx.docPaths[ctx.docId4],
        }
        ctx.ProjectRootDocManager.promises.setRootDocAutomatically.resolves(
          ctx.automaticRootDoc
        )
        result =
          await ctx.ProjectRootDocManager.promises.ensureRootDocumentIsValid(
            ctx.project_id
          )
      })

      it('should return the path and id', ctx => {
        result.should.deep.equal(ctx.automaticRootDoc)
      })

      it('should find the project without doc lines', async function (ctx) {
        ctx.ProjectGetter.promises.getProject
          .calledWith(ctx.project_id)
          .should.equal(true)
      })

      it('should update the project rootDoc_id', function (ctx) {
        ctx.ProjectRootDocManager.promises.setRootDocAutomatically
          .calledWith(ctx.project_id)
          .should.equal(true)
      })
    })

    describe('when the project does not exist', function () {
      beforeEach(async function (ctx) {
        ctx.ProjectGetter.promises.getProject = sinon.stub().resolves(null)
      })

      it('should reject', async function (ctx) {
        await expect(
          ctx.ProjectRootDocManager.promises.ensureRootDocumentIsValid(
            ctx.project_id
          )
        ).to.be.rejectedWith('project not found')
      })
    })
  })
})
