import { logger } from "@coder/logger"
import { readFile, writeFile, stat, utimes } from "fs/promises"
import { Heart, heartbeatTimer } from "../../../src/node/heart"
import { clean, mockLogger, tmpdir } from "../../utils/helpers"

const mockIsActive = (resolveTo: boolean) => jest.fn().mockResolvedValue(resolveTo)

describe("Heart", () => {
  const testName = "heartTests"
  let testDir = ""
  let heart: Heart

  beforeAll(async () => {
    mockLogger()
    await clean(testName)
    testDir = await tmpdir(testName)
  })
  beforeEach(() => {
    heart = new Heart(`${testDir}/shutdown.txt`, mockIsActive(true))
  })
  afterAll(() => {
    jest.restoreAllMocks()
  })
  afterEach(() => {
    jest.resetAllMocks()
    jest.useRealTimers()
    if (heart) {
      heart.dispose()
    }
  })
  it("should write to a file when given a valid file path", async () => {
    // Set up heartbeat file with contents
    const text = "test"
    const pathToFile = `${testDir}/file.txt`
    await writeFile(pathToFile, text)
    const fileContents = await readFile(pathToFile, { encoding: "utf8" })
    // Explicitly set the modified time to 0 so that we can check
    // that the file was indeed modified after calling heart.beat().
    // This works around any potential race conditions.
    // Docs: https://nodejs.org/api/fs.html#fspromisesutimespath-atime-mtime
    await utimes(pathToFile, 0, 0)

    expect(fileContents).toBe(text)

    heart = new Heart(pathToFile, mockIsActive(true))
    await heart.beat()
    // Check that the heart wrote to the heartbeatFilePath and overwrote our text
    const fileContentsAfterBeat = await readFile(pathToFile, { encoding: "utf8" })
    expect(fileContentsAfterBeat).not.toBe(text)
    // Make sure the modified timestamp was updated.
    const fileStatusAfterEdit = await stat(pathToFile)
    expect(fileStatusAfterEdit.mtimeMs).toBeGreaterThan(0)
  })
  it("should log a warning when given an invalid file path", async () => {
    heart = new Heart(`fakeDir/fake.txt`, mockIsActive(false))
    await heart.beat()
    expect(logger.warn).toHaveBeenCalled()
  })
  it("should be active after calling beat", async () => {
    await heart.beat()

    const isAlive = heart.alive()
    expect(isAlive).toBe(true)
  })
  it("should not be active after dispose is called", () => {
    heart.dispose()

    const isAlive = heart.alive()
    expect(isAlive).toBe(false)
  })
  it("should beat twice without warnings", async () => {
    // Use fake timers so we can speed up setTimeout
    jest.useFakeTimers()
    heart = new Heart(`${testDir}/hello.txt`, mockIsActive(true))
    await heart.beat()
    // we need to speed up clocks, timeouts
    // call heartbeat again (and it won't be alive I think)
    // then assert no warnings were called
    jest.runAllTimers()
    expect(logger.warn).not.toHaveBeenCalled()
  })
})

describe("heartbeatTimer", () => {
  beforeAll(() => {
    mockLogger()
  })
  afterAll(() => {
    jest.restoreAllMocks()
  })
  afterEach(() => {
    jest.resetAllMocks()
  })
  it("should call beat when isActive resolves to true", async () => {
    const isActive = true
    const mockIsActive = jest.fn().mockResolvedValue(isActive)
    const mockBeatFn = jest.fn()
    await heartbeatTimer(mockIsActive, mockBeatFn)
    expect(mockIsActive).toHaveBeenCalled()
    expect(mockBeatFn).toHaveBeenCalled()
  })
  it("should log a warning when isActive rejects", async () => {
    const errorMsg = "oh no"
    const error = new Error(errorMsg)
    const mockIsActive = jest.fn().mockRejectedValue(error)
    const mockBeatFn = jest.fn()
    await heartbeatTimer(mockIsActive, mockBeatFn)
    expect(mockIsActive).toHaveBeenCalled()
    expect(mockBeatFn).not.toHaveBeenCalled()
    expect(logger.warn).toHaveBeenCalledWith(errorMsg)
  })
})