import { pathToFileURL } from 'node:url'

import {
  FIXTURE_DOCUMENT,
  FIXTURE_FOLDER,
  FIXTURE_URI,
  REPO_ROOT_FOLDER,
  updateSnapshotUris,
} from '../../../testing/fixtures'
import Analyzer from '../analyser'
import { getDefaultConfiguration } from '../config'
import { initializeParser } from '../parser'
import * as fsUtil from '../util/fs'
import { Logger } from '../util/logger'

const CURRENT_URI = 'dummy-uri.sh'

// if you add a .sh file to testing/fixtures, update this value
const FIXTURE_FILES_MATCHING_GLOB = 17

const defaultConfig = getDefaultConfiguration()

jest.spyOn(Logger.prototype, 'log').mockImplementation(() => {
  // noop
})
const loggerInfo = jest.spyOn(Logger.prototype, 'info')
const loggerWarn = jest.spyOn(Logger.prototype, 'warn')

async function getAnalyzer({
  includeAllWorkspaceSymbols = false,
  workspaceFolder = FIXTURE_FOLDER,
  runBackgroundAnalysis = false,
}: {
  includeAllWorkspaceSymbols?: boolean
  workspaceFolder?: string
  runBackgroundAnalysis?: boolean
}) {
  const parser = await initializeParser()

  const analyzer = new Analyzer({
    parser,
    includeAllWorkspaceSymbols,
    workspaceFolder,
  })
  if (runBackgroundAnalysis) {
    await analyzer.initiateBackgroundAnalysis({
      backgroundAnalysisMaxFiles: defaultConfig.backgroundAnalysisMaxFiles,
      globPattern: defaultConfig.globPattern,
    })
  }
  return analyzer
}

describe('analyze', () => {
  it('returns an empty list of diagnostics for a file with no parsing errors', async () => {
    const analyzer = await getAnalyzer({})
    const diagnostics = analyzer.analyze({
      uri: CURRENT_URI,
      document: FIXTURE_DOCUMENT.INSTALL,
    })
    expect(diagnostics).toEqual([])
    expect(loggerWarn).not.toHaveBeenCalled()
  })

  it('parses files with a missing nodes', async () => {
    const analyzer = await getAnalyzer({})
    const diagnostics = analyzer.analyze({
      uri: CURRENT_URI,
      document: FIXTURE_DOCUMENT.MISSING_NODE,
    })
    expect(diagnostics).toEqual([])
    expect(loggerWarn).toHaveBeenCalledWith(
      'Error while parsing dummy-uri.sh: syntax error',
    )
  })

  it('parses a file with parsing errors', async () => {
    const analyzer = await getAnalyzer({})
    const diagnostics = analyzer.analyze({
      uri: CURRENT_URI,
      document: FIXTURE_DOCUMENT.PARSE_PROBLEMS,
    })
    expect(diagnostics).toEqual([])
    expect(loggerWarn).toHaveBeenCalledWith(
      'Error while parsing dummy-uri.sh: syntax error',
    )
  })

  it('returns a list of diagnostics for a file with sourcing issues', async () => {
    const analyzer = await getAnalyzer({})
    const diagnostics = analyzer.analyze({
      uri: CURRENT_URI,
      document: FIXTURE_DOCUMENT.SOURCING,
    })
    expect(diagnostics).not.toEqual([])
    expect(diagnostics).toMatchInlineSnapshot(`
      [
        {
          "message": "Source command could not be analyzed: non-constant source not supported.

      Note that enabling the configuration flag "includeAllWorkspaceSymbols"
      would include all symbols in the workspace regardless of source commands.
      But be aware that this will lead to false positive suggestions.",
          "range": {
            "end": {
              "character": 16,
              "line": 21,
            },
            "start": {
              "character": 2,
              "line": 21,
            },
          },
          "severity": 3,
          "source": "bash-language-server",
        },
      ]
    `)

    // unless setIncludeAllWorkspaceSymbols set
    analyzer.setIncludeAllWorkspaceSymbols(true)
    const diagnostics2 = analyzer.analyze({
      uri: CURRENT_URI,
      document: FIXTURE_DOCUMENT.SOURCING,
    })
    expect(diagnostics2).toEqual([])
  })
})

describe('findDeclarationLocations', () => {
  it('returns a location to a file if word is the path in a sourcing statement', async () => {
    const analyzer = await getAnalyzer({})
    const document = FIXTURE_DOCUMENT.SOURCING
    const { uri } = document
    analyzer.analyze({ uri, document })
    const result = analyzer.findDeclarationLocations({
      uri,
      word: './extension.inc',
      position: { character: 10, line: 2 },
    })
    expect(updateSnapshotUris(result)).toMatchInlineSnapshot(`
      [
        {
          "range": {
            "end": {
              "character": 0,
              "line": 0,
            },
            "start": {
              "character": 0,
              "line": 0,
            },
          },
          "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/extension.inc",
        },
      ]
    `)
  })

  it('returns a location to a file if word is the path in a sourcing statement (resolved by using the workspacePath)', async () => {
    const document = FIXTURE_DOCUMENT.SOURCING
    const { uri } = document

    const analyzer = await getAnalyzer({ workspaceFolder: REPO_ROOT_FOLDER })

    analyzer.analyze({ uri, document })
    const result = analyzer.findDeclarationLocations({
      uri,
      word: './scripts/tag-release.inc',
      position: { character: 10, line: 16 },
    })
    expect(updateSnapshotUris(result)).toMatchInlineSnapshot(`
      [
        {
          "range": {
            "end": {
              "character": 0,
              "line": 0,
            },
            "start": {
              "character": 0,
              "line": 0,
            },
          },
          "uri": "file://__REPO_ROOT_FOLDER__/scripts/tag-release.inc",
        },
      ]
    `)
  })

  it('returns a local reference if definition is found', async () => {
    const analyzer = await getAnalyzer({})
    analyzer.analyze({ uri: CURRENT_URI, document: FIXTURE_DOCUMENT.INSTALL })
    const result = analyzer.findDeclarationLocations({
      position: { character: 1, line: 148 },
      uri: CURRENT_URI,
      word: 'node_version',
    })
    expect(updateSnapshotUris(result)).toMatchInlineSnapshot(`
      [
        {
          "range": {
            "end": {
              "character": 37,
              "line": 148,
            },
            "start": {
              "character": 0,
              "line": 148,
            },
          },
          "uri": "dummy-uri.sh",
        },
      ]
    `)
  })

  it('returns local declarations', async () => {
    const analyzer = await getAnalyzer({})
    const result = analyzer.findDeclarationLocations({
      position: { character: 12, line: 12 },
      uri: FIXTURE_URI.SCOPE,
      word: 'X',
    })
    expect(updateSnapshotUris(result)).toMatchInlineSnapshot(`
      [
        {
          "range": {
            "end": {
              "character": 17,
              "line": 12,
            },
            "start": {
              "character": 10,
              "line": 12,
            },
          },
          "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/scope.sh",
        },
      ]
    `)
  })

  it('returns local declarations for loop variables', async () => {
    const analyzer = await getAnalyzer({})
    const result = analyzer.findDeclarationLocations({
      position: { character: 18, line: 39 },
      uri: FIXTURE_URI.SCOPE,
      word: 'i',
    })
    expect(updateSnapshotUris(result)).toMatchInlineSnapshot(`
      [
        {
          "range": {
            "end": {
              "character": 5,
              "line": 37,
            },
            "start": {
              "character": 4,
              "line": 37,
            },
          },
          "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/scope.sh",
        },
      ]
    `)
  })
})

describe('findReferences', () => {
  it('returns empty list if parameter is not found', async () => {
    const analyzer = await getAnalyzer({})
    analyzer.analyze({ uri: CURRENT_URI, document: FIXTURE_DOCUMENT.INSTALL })
    const result = analyzer.findReferences('foobar')
    expect(result).toEqual([])
  })

  it('returns a list of locations if parameter is found', async () => {
    const analyzer = await getAnalyzer({})
    analyzer.analyze({ uri: CURRENT_URI, document: FIXTURE_DOCUMENT.INSTALL })
    const result = analyzer.findReferences('node_version')
    expect(result).not.toEqual([])
    expect(result).toMatchSnapshot()
  })
})

describe('getDeclarationsForUri', () => {
  it('returns empty list if uri is not found', async () => {
    const analyzer = await getAnalyzer({})
    analyzer.analyze({ uri: CURRENT_URI, document: FIXTURE_DOCUMENT.INSTALL })
    const result = analyzer.getDeclarationsForUri({ uri: 'foobar.sh' })
    expect(result).toEqual([])
  })

  it('returns a list of SymbolInformation if uri is found', async () => {
    const analyzer = await getAnalyzer({})
    analyzer.analyze({ uri: CURRENT_URI, document: FIXTURE_DOCUMENT.INSTALL })
    const result = analyzer.getDeclarationsForUri({ uri: CURRENT_URI })
    expect(result).not.toEqual([])
    expect(result).toMatchSnapshot()
  })

  it('issue 101', async () => {
    const analyzer = await getAnalyzer({})
    analyzer.analyze({ uri: CURRENT_URI, document: FIXTURE_DOCUMENT.ISSUE101 })
    const result = analyzer.getDeclarationsForUri({ uri: CURRENT_URI })
    expect(result).not.toEqual([])
    expect(result).toMatchSnapshot()
  })
})

describe('findAllSourcedUris', () => {
  it('returns references to sourced files', async () => {
    const analyzer = await getAnalyzer({
      runBackgroundAnalysis: true,
      workspaceFolder: pathToFileURL(REPO_ROOT_FOLDER).href,
    })

    const result = analyzer.findAllSourcedUris({ uri: FIXTURE_URI.SOURCING })
    expect(result).toEqual(
      new Set([
        `file://${REPO_ROOT_FOLDER}/scripts/tag-release.inc`, // resolved based on repoRootFolder
        `file://${FIXTURE_FOLDER}issue101.sh`, // resolved based on current file
        `file://${FIXTURE_FOLDER}extension.inc`, // resolved based on current file
      ]),
    )
  })

  it('returns references to sourced files without file extension', async () => {
    const analyzer = await getAnalyzer({ runBackgroundAnalysis: true })

    // Parse the file without extension
    analyzer.analyze({
      uri: FIXTURE_URI.MISSING_EXTENSION,
      document: FIXTURE_DOCUMENT.MISSING_EXTENSION,
    })

    const result = analyzer.findAllSourcedUris({ uri: FIXTURE_URI.MISSING_EXTENSION })
    expect(result).toEqual(
      new Set([
        `file://${FIXTURE_FOLDER}extension.inc`,
        `file://${FIXTURE_FOLDER}issue101.sh`,
      ]),
    )
  })
})

describe('wordAtPoint', () => {
  it('returns current word at a given point', async () => {
    const analyzer = await getAnalyzer({})
    analyzer.analyze({ uri: CURRENT_URI, document: FIXTURE_DOCUMENT.INSTALL })
    expect(analyzer.wordAtPoint(CURRENT_URI, 25, 0)).toEqual(null)
    expect(analyzer.wordAtPoint(CURRENT_URI, 25, 1)).toEqual(null)
    expect(analyzer.wordAtPoint(CURRENT_URI, 25, 2)).toEqual(null)
    expect(analyzer.wordAtPoint(CURRENT_URI, 25, 3)).toEqual(null)
    expect(analyzer.wordAtPoint(CURRENT_URI, 25, 4)).toEqual('rm')
    expect(analyzer.wordAtPoint(CURRENT_URI, 25, 5)).toEqual('rm')
    expect(analyzer.wordAtPoint(CURRENT_URI, 25, 6)).toEqual(null)
    expect(analyzer.wordAtPoint(CURRENT_URI, 25, 7)).toEqual('npm-install-')

    expect(analyzer.wordAtPoint(CURRENT_URI, 24, 2)).toEqual('else')
    expect(analyzer.wordAtPoint(CURRENT_URI, 24, 3)).toEqual('else')
    expect(analyzer.wordAtPoint(CURRENT_URI, 24, 5)).toEqual('else')
    expect(analyzer.wordAtPoint(CURRENT_URI, 24, 7)).toEqual(null)

    expect(analyzer.wordAtPoint(CURRENT_URI, 30, 1)).toEqual(null)

    expect(analyzer.wordAtPoint(CURRENT_URI, 30, 2)).toEqual('ret')
    expect(analyzer.wordAtPoint(CURRENT_URI, 30, 3)).toEqual('ret')
    expect(analyzer.wordAtPoint(CURRENT_URI, 30, 4)).toEqual('ret')
    expect(analyzer.wordAtPoint(CURRENT_URI, 30, 5)).toEqual('=')

    expect(analyzer.wordAtPoint(CURRENT_URI, 38, 5)).toEqual('configures')
  })
})

describe('commandNameAtPoint', () => {
  it('returns current command name at a given point', async () => {
    const analyzer = await getAnalyzer({})
    analyzer.analyze({ uri: CURRENT_URI, document: FIXTURE_DOCUMENT.INSTALL })
    expect(analyzer.commandNameAtPoint(CURRENT_URI, 15, 0)).toEqual(null)

    expect(analyzer.commandNameAtPoint(CURRENT_URI, 20, 2)).toEqual('curl')
    expect(analyzer.commandNameAtPoint(CURRENT_URI, 20, 15)).toEqual('curl')
    expect(analyzer.commandNameAtPoint(CURRENT_URI, 20, 19)).toEqual('curl')

    expect(analyzer.commandNameAtPoint(CURRENT_URI, 26, 4)).toEqual('echo')
    expect(analyzer.commandNameAtPoint(CURRENT_URI, 26, 9)).toEqual('echo')

    expect(analyzer.commandNameAtPoint(CURRENT_URI, 38, 13)).toEqual('env')
    expect(analyzer.commandNameAtPoint(CURRENT_URI, 38, 24)).toEqual('grep')
    expect(analyzer.commandNameAtPoint(CURRENT_URI, 38, 44)).toEqual('sed')
  })
})

describe('findDeclarationsMatchingWord', () => {
  it('returns a list of symbols across the workspace when includeAllWorkspaceSymbols is true', async () => {
    const analyzer = await getAnalyzer({
      includeAllWorkspaceSymbols: true,
      runBackgroundAnalysis: true,
    })

    expect(
      updateSnapshotUris(
        analyzer.findDeclarationsMatchingWord({
          word: 'npm_config_logl',
          uri: FIXTURE_URI.INSTALL,
          exactMatch: false,
          position: { line: 1000, character: 0 },
        }),
      ),
    ).toMatchInlineSnapshot(`
      [
        {
          "kind": 13,
          "location": {
            "range": {
              "end": {
                "character": 27,
                "line": 40,
              },
              "start": {
                "character": 0,
                "line": 40,
              },
            },
            "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/install.sh",
          },
          "name": "npm_config_loglevel",
        },
      ]
    `)

    expect(
      analyzer.findDeclarationsMatchingWord({
        word: 'xxxxxxxx',
        uri: FIXTURE_URI.INSTALL,
        exactMatch: false,
        position: { line: 1000, character: 0 },
      }),
    ).toMatchInlineSnapshot(`[]`)

    expect(
      updateSnapshotUris(
        analyzer.findDeclarationsMatchingWord({
          word: 'BLU',
          uri: FIXTURE_URI.INSTALL,
          exactMatch: false,
          position: { line: 6, character: 9 },
        }),
      ),
    ).toMatchInlineSnapshot(`
      [
        {
          "kind": 13,
          "location": {
            "range": {
              "end": {
                "character": 25,
                "line": 6,
              },
              "start": {
                "character": 6,
                "line": 6,
              },
            },
            "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/extension.inc",
          },
          "name": "BLUE",
        },
      ]
    `)

    expect(
      updateSnapshotUris(
        analyzer.findDeclarationsMatchingWord({
          word: 'BLU',
          uri: FIXTURE_URI.SOURCING,
          exactMatch: false,
          position: { line: 6, character: 9 },
        }),
      ),
    ).toMatchInlineSnapshot(`
      [
        {
          "kind": 13,
          "location": {
            "range": {
              "end": {
                "character": 25,
                "line": 6,
              },
              "start": {
                "character": 6,
                "line": 6,
              },
            },
            "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/extension.inc",
          },
          "name": "BLUE",
        },
      ]
    `)
  })

  it('returns a list of symbols accessible to the uri when includeAllWorkspaceSymbols is false', async () => {
    const analyzer = await getAnalyzer({
      includeAllWorkspaceSymbols: false,
      runBackgroundAnalysis: true,
    })

    expect(
      analyzer.findDeclarationsMatchingWord({
        word: 'BLU',
        uri: FIXTURE_URI.INSTALL,
        exactMatch: false,
        position: { line: 1000, character: 0 },
      }),
    ).toMatchInlineSnapshot(`[]`)

    expect(
      updateSnapshotUris(
        analyzer.findDeclarationsMatchingWord({
          word: 'BLU',
          uri: FIXTURE_URI.SOURCING,
          exactMatch: false,
          position: { line: 6, character: 9 },
        }),
      ),
    ).toMatchInlineSnapshot(`
      [
        {
          "kind": 13,
          "location": {
            "range": {
              "end": {
                "character": 25,
                "line": 6,
              },
              "start": {
                "character": 6,
                "line": 6,
              },
            },
            "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/extension.inc",
          },
          "name": "BLUE",
        },
      ]
    `)
  })

  it('resolves sourced file not covered by the background analysis glob', async () => {
    async function expectThatSourcingWorksWhenIncludeAllWorkspaceSymbolsIs(v: boolean) {
      const analyzer = await getAnalyzer({
        runBackgroundAnalysis: true,
        includeAllWorkspaceSymbols: v,
      })

      expect(
        analyzer.findDeclarationsMatchingWord({
          word: 'XXX',
          uri: FIXTURE_URI.SOURCING2,
          exactMatch: true,
          position: { line: 2, character: 21 }, // XXX
        }),
      ).toHaveLength(1)
    }

    await expectThatSourcingWorksWhenIncludeAllWorkspaceSymbolsIs(false)
    await expectThatSourcingWorksWhenIncludeAllWorkspaceSymbolsIs(true)
  })

  it('returns symbols depending on the scope', async () => {
    const analyzer = await getAnalyzer({
      includeAllWorkspaceSymbols: false,
    })

    const findWordFromLine = (word: string, line: number) =>
      analyzer.findDeclarationsMatchingWord({
        word,
        uri: FIXTURE_URI.SCOPE,
        exactMatch: true,
        position: { line, character: 0 },
      })

    // Variable or function defined yet
    expect(findWordFromLine('X', 0)).toEqual([])
    expect(findWordFromLine('f', 0)).toEqual([])

    // First definition
    expect(updateSnapshotUris(findWordFromLine('X', 3))).toMatchInlineSnapshot(`
      [
        {
          "kind": 13,
          "location": {
            "range": {
              "end": {
                "character": 9,
                "line": 2,
              },
              "start": {
                "character": 0,
                "line": 2,
              },
            },
            "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/scope.sh",
          },
          "name": "X",
        },
      ]
    `)

    // Local variable definition
    expect(updateSnapshotUris(findWordFromLine('X', 13))).toMatchInlineSnapshot(`
      [
        {
          "containerName": "g",
          "kind": 13,
          "location": {
            "range": {
              "end": {
                "character": 17,
                "line": 12,
              },
              "start": {
                "character": 10,
                "line": 12,
              },
            },
            "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/scope.sh",
          },
          "name": "X",
        },
      ]
    `)

    // Local function definition
    expect(updateSnapshotUris(findWordFromLine('f', 23))).toMatchInlineSnapshot(`
      [
        {
          "containerName": "g",
          "kind": 12,
          "location": {
            "range": {
              "end": {
                "character": 5,
                "line": 21,
              },
              "start": {
                "character": 4,
                "line": 18,
              },
            },
            "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/scope.sh",
          },
          "name": "f",
        },
      ]
    `)

    // Last definition
    expect(updateSnapshotUris(findWordFromLine('X', 1000))).toMatchInlineSnapshot(`
      [
        {
          "kind": 13,
          "location": {
            "range": {
              "end": {
                "character": 9,
                "line": 4,
              },
              "start": {
                "character": 0,
                "line": 4,
              },
            },
            "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/scope.sh",
          },
          "name": "X",
        },
      ]
    `)

    expect(updateSnapshotUris(findWordFromLine('f', 1000))).toMatchInlineSnapshot(`
      [
        {
          "kind": 12,
          "location": {
            "range": {
              "end": {
                "character": 1,
                "line": 30,
              },
              "start": {
                "character": 0,
                "line": 7,
              },
            },
            "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/scope.sh",
          },
          "name": "f",
        },
      ]
    `)

    // Global variable defined inside a function
    expect(updateSnapshotUris(findWordFromLine('GLOBAL_1', 1000))).toMatchInlineSnapshot(`
      [
        {
          "containerName": "g",
          "kind": 13,
          "location": {
            "range": {
              "end": {
                "character": 23,
                "line": 13,
              },
              "start": {
                "character": 4,
                "line": 13,
              },
            },
            "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/scope.sh",
          },
          "name": "GLOBAL_1",
        },
      ]
    `)
  })
})

describe('commentsAbove', () => {
  it('returns a string of a comment block above a line', async () => {
    const analyzer = await getAnalyzer({})
    analyzer.analyze({ uri: CURRENT_URI, document: FIXTURE_DOCUMENT.COMMENT_DOC })
    expect(analyzer.commentsAbove(CURRENT_URI, 22)).toEqual(
      '```txt\ndoc for func_one\n```',
    )
  })

  it('handles line breaks in comments', async () => {
    const analyzer = await getAnalyzer({})
    analyzer.analyze({ uri: CURRENT_URI, document: FIXTURE_DOCUMENT.COMMENT_DOC })
    expect(analyzer.commentsAbove(CURRENT_URI, 28)).toEqual(
      '```txt\ndoc for func_two\nhas two lines\n```',
    )
  })

  it('only returns connected comments', async () => {
    const analyzer = await getAnalyzer({})
    analyzer.analyze({ uri: CURRENT_URI, document: FIXTURE_DOCUMENT.COMMENT_DOC })
    expect(analyzer.commentsAbove(CURRENT_URI, 36)).toEqual(
      '```txt\ndoc for func_three\n```',
    )
  })

  it('returns null if no comment found', async () => {
    const analyzer = await getAnalyzer({})
    analyzer.analyze({ uri: CURRENT_URI, document: FIXTURE_DOCUMENT.COMMENT_DOC })
    expect(analyzer.commentsAbove(CURRENT_URI, 45)).toEqual(null)
  })

  it('works for variables', async () => {
    const analyzer = await getAnalyzer({})
    analyzer.analyze({ uri: CURRENT_URI, document: FIXTURE_DOCUMENT.COMMENT_DOC })
    expect(analyzer.commentsAbove(CURRENT_URI, 42)).toEqual(
      '```txt\nworks for variables\n```',
    )
  })

  it('returns connected comments with empty comment line', async () => {
    const analyzer = await getAnalyzer({})
    analyzer.analyze({ uri: CURRENT_URI, document: FIXTURE_DOCUMENT.COMMENT_DOC })
    expect(analyzer.commentsAbove(CURRENT_URI, 51)).toEqual(
      '```txt\nthis is also included\n\ndoc for func_four\n```',
    )
  })
})

describe('initiateBackgroundAnalysis', () => {
  it('finds bash files', async () => {
    jest.spyOn(Date, 'now').mockImplementation(() => 0)

    const analyzer = await getAnalyzer({})

    const { filesParsed } = await analyzer.initiateBackgroundAnalysis({
      backgroundAnalysisMaxFiles: defaultConfig.backgroundAnalysisMaxFiles,
      globPattern: defaultConfig.globPattern,
    })

    expect(loggerWarn).toHaveBeenCalled()
    expect(loggerWarn.mock.calls).toEqual([
      [expect.stringContaining('missing-node.sh: syntax error')],
      [expect.stringContaining('missing-node2.sh: syntax error')],
      [expect.stringContaining('not-a-shell-script.sh: syntax error')],
      [expect.stringContaining('parse-problems.sh: syntax error')],
    ])

    // Intro, stats on glob, one file skipped due to shebang, and outro
    expect(filesParsed).toEqual(FIXTURE_FILES_MATCHING_GLOB)

    expect(loggerInfo).toHaveBeenNthCalledWith(
      1,
      expect.stringContaining('BackgroundAnalysis: resolving glob'),
    )
  })

  it('handles glob errors', async () => {
    jest
      .spyOn(fsUtil, 'getFilePaths')
      .mockImplementation(() => Promise.reject(new Error('BOOM')))

    const analyzer = await getAnalyzer({})

    const { filesParsed } = await analyzer.initiateBackgroundAnalysis({
      backgroundAnalysisMaxFiles: defaultConfig.backgroundAnalysisMaxFiles,
      globPattern: defaultConfig.globPattern,
    })

    expect(loggerWarn).toHaveBeenCalledWith(expect.stringContaining('BOOM'))
    expect(filesParsed).toEqual(0)
  })

  it('allows skipping the analysis', async () => {
    jest.spyOn(Date, 'now').mockImplementation(() => 0)

    const analyzer = await getAnalyzer({})

    const { filesParsed } = await analyzer.initiateBackgroundAnalysis({
      backgroundAnalysisMaxFiles: 0,
      globPattern: defaultConfig.globPattern,
    })

    expect(loggerWarn).not.toHaveBeenCalled()

    expect(filesParsed).toEqual(0)
  })
})

describe('getAllVariables', () => {
  it('returns all variable symbols', async () => {
    const document = FIXTURE_DOCUMENT.SOURCING
    const { uri } = document

    const analyzer = await getAnalyzer({ workspaceFolder: REPO_ROOT_FOLDER })

    // NOTE: no background analysis

    analyzer.analyze({ uri, document })

    expect(
      updateSnapshotUris(
        analyzer.getAllVariables({
          uri,
          position: { line: 20, character: 0 },
        }),
      ),
    ).toMatchInlineSnapshot(`
      [
        {
          "kind": 13,
          "location": {
            "range": {
              "end": {
                "character": 16,
                "line": 10,
              },
              "start": {
                "character": 0,
                "line": 10,
              },
            },
            "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/sourcing.sh",
          },
          "name": "BOLD",
        },
        {
          "kind": 13,
          "location": {
            "range": {
              "end": {
                "character": 18,
                "line": 4,
              },
              "start": {
                "character": 0,
                "line": 4,
              },
            },
            "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/extension.inc",
          },
          "name": "RED",
        },
        {
          "kind": 13,
          "location": {
            "range": {
              "end": {
                "character": 20,
                "line": 5,
              },
              "start": {
                "character": 0,
                "line": 5,
              },
            },
            "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/extension.inc",
          },
          "name": "GREEN",
        },
        {
          "kind": 13,
          "location": {
            "range": {
              "end": {
                "character": 25,
                "line": 6,
              },
              "start": {
                "character": 6,
                "line": 6,
              },
            },
            "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/extension.inc",
          },
          "name": "BLUE",
        },
        {
          "kind": 13,
          "location": {
            "range": {
              "end": {
                "character": 23,
                "line": 7,
              },
              "start": {
                "character": 7,
                "line": 7,
              },
            },
            "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/extension.inc",
          },
          "name": "BOLD",
        },
        {
          "kind": 13,
          "location": {
            "range": {
              "end": {
                "character": 19,
                "line": 10,
              },
              "start": {
                "character": 2,
                "line": 10,
              },
            },
            "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/extension.inc",
          },
          "name": "RESET",
        },
        {
          "kind": 13,
          "location": {
            "range": {
              "end": {
                "character": 14,
                "line": 25,
              },
              "start": {
                "character": 5,
                "line": 25,
              },
            },
            "uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/extension.inc",
          },
          "name": "FILE_PATH",
        },
      ]
    `)
  })
})
