import o from "@tutao/otest"
import { createDataFile } from "../../../../src/common/api/common/DataFile.js"
import { DesktopFileFacade } from "../../../../src/common/desktop/files/DesktopFileFacade.js"
import { ApplicationWindow } from "../../../../src/common/desktop/ApplicationWindow.js"
import { func, matchers, object, verify, when } from "testdouble"
import { ElectronExports, FsExports, PathExports } from "../../../../src/common/desktop/ElectronExportTypes.js"
import { NotFoundError, PreconditionFailedError, TooManyRequestsError } from "../../../../src/common/api/common/error/RestError.js"
import type fs from "node:fs"
import { assertThrows } from "@tutao/tutanota-test-utils"
import { stringToUtf8Uint8Array } from "@tutao/tutanota-utils"
import { DesktopConfig } from "../../../../src/common/desktop/config/DesktopConfig.js"
import { DesktopUtils } from "../../../../src/common/desktop/DesktopUtils.js"
import { DateProvider } from "../../../../src/common/api/common/DateProvider.js"
import { TempFs } from "../../../../src/common/desktop/files/TempFs.js"
import { BuildConfigKey, DesktopConfigKey } from "../../../../src/common/desktop/config/ConfigKeys.js"
import { HttpMethod } from "../../../../src/common/api/common/EntityFunctions"
import { FetchImpl, FetchResult } from "../../../../src/common/desktop/net/NetAgent"
import { CommandExecutor } from "../../../../src/common/desktop/CommandExecutor"

const DEFAULT_DOWNLOAD_PATH = "/a/download/path/"

o.spec("DesktopFileFacade", function () {
	let win: ApplicationWindow
	let conf: DesktopConfig
	let du: DesktopUtils
	let dp: DateProvider
	let fetch: FetchImpl
	let electron: ElectronExports
	let fs: FsExports
	let tfs: TempFs
	let ff: DesktopFileFacade
	let path: PathExports
	let executor: CommandExecutor
	let process: Writeable<Partial<NodeJS.Process>>

	o.beforeEach(function () {
		win = object()
		fetch = func() as FetchImpl
		fs = object()
		tfs = object()
		path = object()
		fs.promises = object()
		process = {
			env: {},
		}
		when(fs.promises.stat(matchers.anything())).thenResolve({ size: 42 })
		electron = object()
		// @ts-ignore read-only prop
		electron["shell"] = object()
		// @ts-ignore read-only prop
		electron["dialog"] = object()
		process.platform = "linux"

		conf = object()
		du = object()
		dp = object()
		executor = object()

		ff = new DesktopFileFacade(win, conf, dp, fetch, electron, tfs, fs, path, executor, process as NodeJS.Process)
	})
	o.spec("saveDataFile", function () {
		o("when there's no existing file it will be simply written", async function () {
			const dataFile = createDataFile("blob", "application/octet-stream", new Uint8Array([1]))
			when(fs.promises.readdir(matchers.anything())).thenResolve(["somethingelse"])
			when(fs.promises.mkdir("/tutanota/tmp/path/download", { recursive: true })).thenResolve(undefined)
			when(fs.promises.writeFile("/tutanota/tmp/path/download/blob", dataFile.data)).thenResolve()
			await ff.writeTempDataFile(dataFile)
		})

		o("with default download path but file exists -> nonclobbering name is chosen", async function () {
			const e = new Error() as any
			e.code = "EEXISTS"
			const dataFile = createDataFile("blob", "application/octet-stream", new Uint8Array([1]))
			when(fs.promises.writeFile("/tutanota/tmp/path/download/blob", matchers.anything())).thenReject(e)
			when(fs.promises.readdir(matchers.anything())).thenResolve(["blob"])
			when(fs.promises.mkdir("/tutanota/tmp/path/download", { recursive: true })).thenResolve(undefined)
			when(fs.promises.writeFile("/tutanota/tmp/path/download/blob-1", dataFile.data)).thenResolve()
			await ff.writeTempDataFile(dataFile)
		})
	})

	o.spec("download", function () {
		o("no error", async function () {
			const headers = { v: "foo", accessToken: "bar" }
			const expectedFilePath = "/tutanota/tmp/path/encrypted/nativelyDownloadedFile"
			const response: FetchResult = mockResponse(200, { responseBody: new Uint8Array() })
			const ws: fs.WriteStream = mockWriteStream(response)
			when(fs.createWriteStream(expectedFilePath, { emitClose: true })).thenReturn(ws)
			when(
				fetch(urlMatches(new URL("some://url/file")), {
					method: "GET",
					headers,
				}),
			).thenResolve(response)
			// @ts-ignore callback omit
			when(ws.on("finish")).thenCallback(undefined, undefined)
			when(tfs.ensureEncryptedDir()).thenResolve("/tutanota/tmp/path/encrypted")

			const downloadResult = await ff.download("some://url/file", "nativelyDownloadedFile", headers)
			o(downloadResult.statusCode).equals(200)
			o(downloadResult.encryptedFileUri).equals(expectedFilePath)
		})

		o("404 error gets returned", async function () {
			const headers = {
				v: "foo",
				accessToken: "bar",
			}

			const errorId = "123"
			const response: FetchResult = mockResponse(NotFoundError.CODE, {
				responseHeaders: {
					"error-id": errorId,
				},
			})
			when(fetch(matchers.anything(), matchers.anything())).thenResolve(response)
			const result = await ff.download("some://url/file", "nativelyDownloadedFile", headers)

			o(result).deepEquals({
				statusCode: 404,
				errorId,
				precondition: null,
				suspensionTime: null,
				encryptedFileUri: null,
			})
			verify(fs.createWriteStream(matchers.anything(), matchers.anything()), { times: 0 })
		})

		o("retry-after", async function () {
			const retryAfter = "20"
			const errorId = "123"

			const response: FetchResult = mockResponse(TooManyRequestsError.CODE, {
				responseHeaders: {
					"error-id": errorId,
					"retry-after": retryAfter,
				},
			})
			when(fetch(matchers.anything(), matchers.anything())).thenResolve(response)

			const headers = { v: "foo", accessToken: "bar" }
			const result = await ff.download("some://url/file", "nativelyDownloadedFile", headers)

			o(result).deepEquals({
				statusCode: TooManyRequestsError.CODE,
				errorId,
				precondition: null,
				suspensionTime: retryAfter,
				encryptedFileUri: null,
			})
			verify(fs.createWriteStream(matchers.anything(), matchers.anything()), { times: 0 })
		})

		o("suspension", async function () {
			const headers = { v: "foo", accessToken: "bar" }
			const errorId = "123"
			const retryAfter = "20"
			const response: FetchResult = mockResponse(TooManyRequestsError.CODE, {
				responseHeaders: {
					"error-id": errorId,
					"suspension-time": retryAfter,
				},
			})
			when(fetch(matchers.anything(), matchers.anything())).thenResolve(response)

			const result = await ff.download("some://url/file", "nativelyDownloadedFile", headers)

			o(result).deepEquals({
				statusCode: TooManyRequestsError.CODE,
				errorId,
				precondition: null,
				suspensionTime: retryAfter,
				encryptedFileUri: null,
			})
			verify(fs.createWriteStream(matchers.anything(), matchers.anything()), { times: 0 })
		})

		o("precondition", async function () {
			const headers = { v: "foo", accessToken: "bar" }
			const errorId = "123"
			const precondition = "a.2"
			const response: FetchResult = mockResponse(PreconditionFailedError.CODE, {
				responseHeaders: {
					"error-id": errorId,
					precondition: precondition,
				},
			})
			when(fetch(matchers.anything(), matchers.anything())).thenResolve(response)

			const result = await ff.download("some://url/file", "nativelyDownloadedFile", headers)

			o(result).deepEquals({
				statusCode: PreconditionFailedError.CODE,
				errorId,
				precondition,
				suspensionTime: null,
				encryptedFileUri: null,
			})
			verify(fs.createWriteStream(matchers.anything(), matchers.anything()), { times: 0 })
		})

		o("IO error during download leads to cleanup and error is thrown", async function () {
			const headers = { v: "foo", accessToken: "bar" }
			const response: FetchResult = mockResponse(200, { responseBody: new Uint8Array() })
			when(fetch(matchers.anything(), matchers.anything())).thenResolve(response)
			const error = new Error("Test! I/O error")
			const ws = mockWriteStream()
			when(ws.on("finish", matchers.anything())).thenThrow(error)
			when(fs.createWriteStream(matchers.anything(), matchers.anything())).thenReturn(ws)
			when(tfs.ensureEncryptedDir()).thenResolve("/tutanota/tmp/path/encrypted")

			const e = await assertThrows(Error, () => ff.download("some://url/file", "nativelyDownloadedFile", headers))
			o(e).equals(error)
			verify(fs.promises.unlink("/tutanota/tmp/path/encrypted/nativelyDownloadedFile"), { times: 1 })
		})
	})

	o.spec("upload", function () {
		const fileToUploadPath = "/tutnaota/tmp/path/encrypted/toUpload.txt"
		const targetUrl = "https://test.tutanota.com/rest/for/a/bit"

		o("when there's no error it uploads correct data and returns the right result", async function () {
			const body = stringToUtf8Uint8Array("BODY")
			const response = mockResponse(200, { responseBody: body })
			const headers = {
				blobAccessToken: "1236",
			}
			const fileStreamMock = mockReadStream()
			when(fs.createReadStream(fileToUploadPath)).thenReturn(fileStreamMock)
			when(
				fetch(urlMatches(new URL(targetUrl)), {
					method: HttpMethod.POST,
					headers,
					body: fileStreamMock,
				}),
			).thenResolve(response)
			const uploadResult = await ff.upload(fileToUploadPath, targetUrl, HttpMethod.POST, headers)

			o(uploadResult.statusCode).equals(200)
			o(uploadResult.errorId).equals(null)
			o(uploadResult.precondition).equals(null)
			o(uploadResult.suspensionTime).equals(null)
			o(Array.from(uploadResult.responseBody)).deepEquals(Array.from(body))
		})

		o("when 404 is returned it returns correct result", async function () {
			const errorId = "123"
			const response = mockResponse(404, { responseHeaders: { "error-id": errorId } })
			when(fetch(matchers.anything(), matchers.anything())).thenResolve(response)
			const uploadResult = await ff.upload(fileToUploadPath, targetUrl, HttpMethod.POST, {})
			o(uploadResult.statusCode).equals(404)
			o(uploadResult.errorId).equals(errorId)
			o(uploadResult.precondition).equals(null)
			o(uploadResult.suspensionTime).equals(null)
			o(Array.from(uploadResult.responseBody)).deepEquals([])
		})

		o("when retry-after is returned, it is propagated", async function () {
			const retryAFter = "20"
			const errorId = "123"
			const response = mockResponse(TooManyRequestsError.CODE, {
				responseHeaders: {
					"error-id": errorId,
					"retry-after": retryAFter,
				},
			})
			when(fetch(matchers.anything(), matchers.anything())).thenResolve(response)

			const uploadResult = await ff.upload(fileToUploadPath, targetUrl, HttpMethod.POST, {})

			o(uploadResult.statusCode).equals(TooManyRequestsError.CODE)
			o(uploadResult.errorId).equals(errorId)
			o(uploadResult.precondition).equals(null)
			o(uploadResult.suspensionTime).equals(retryAFter)
			o(Array.from(uploadResult.responseBody)).deepEquals([])
		})

		o("when suspension-time is returned, it is propagated", async function () {
			const retryAFter = "20"
			const errorId = "123"
			const response = mockResponse(TooManyRequestsError.CODE, {
				responseHeaders: {
					"error-id": errorId,
					"suspension-time": retryAFter,
				},
			})
			when(fetch(matchers.anything(), matchers.anything())).thenResolve(response)
			const uploadResult = await ff.upload(fileToUploadPath, targetUrl, HttpMethod.POST, {})

			o(uploadResult.statusCode).equals(TooManyRequestsError.CODE)
			o(uploadResult.errorId).equals(errorId)
			o(uploadResult.precondition).equals(null)
			o(uploadResult.suspensionTime).equals(retryAFter)
			o(Array.from(uploadResult.responseBody)).deepEquals([])
		})

		o("when precondition-time is returned, it is propagated", async function () {
			const precondition = "a.2"
			const errorId = "123"
			const response = mockResponse(PreconditionFailedError.CODE, {
				responseHeaders: {
					"error-id": errorId,
					precondition: precondition,
				},
			})
			when(fetch(matchers.anything(), matchers.anything())).thenResolve(response)

			const uploadResult = await ff.upload(fileToUploadPath, targetUrl, HttpMethod.POST, {})

			o(uploadResult.statusCode).equals(PreconditionFailedError.CODE)
			o(uploadResult.errorId).equals(errorId)
			o(uploadResult.precondition).equals(precondition)
			o(uploadResult.suspensionTime).equals(null)
			o(Array.from(uploadResult.responseBody)).deepEquals([])
		})
	})

	o.spec("open", function () {
		o.spec("open on linux", () => {
			o.test("open on ubuntu", async function () {
				when(electron.shell.openPath("/some/folder/file")).thenReject(new Error("wrong function"))
				when(executor.run(matchers.anything())).thenResolve({})
				process.env!.ORIGINAL_XDG_CURRENT_DESKTOP = "original ;)"
				process.env!.SOMETHING_ELSE = "something else!"
				await ff.open("/some/folder/file")

				verify(
					executor.run({
						executable: "xdg-open",
						args: ["/some/folder/file"],
						env: {
							XDG_CURRENT_DESKTOP: "original ;)",
							ORIGINAL_XDG_CURRENT_DESKTOP: "original ;)",
							SOMETHING_ELSE: "something else!",
						},
					}),
				)
			})
			o.test("open on non ubuntu", async function () {
				when(electron.shell.openPath("/some/folder/file")).thenReject(new Error("wrong function"))
				when(executor.run(matchers.anything())).thenResolve({})
				process.env!.ORIGINAL_XDG_CURRENT_DESKTOP = undefined
				process.env!.SOMETHING_ELSE = "something else!"
				await ff.open("/some/folder/file")

				verify(
					executor.run({
						executable: "xdg-open",
						args: ["/some/folder/file"],
						env: undefined,
					}),
				)
			})
		})

		o.test("open on windows", async function () {
			process.platform = "win32"
			when(electron.dialog.showMessageBox(matchers.anything())).thenReturn(
				Promise.resolve({
					response: 1,
					checkboxChecked: false,
				}),
			)
			await ff.open("exec.exe")
			verify(electron.shell.openPath(matchers.anything()), { times: 0 })
		})
	})

	o.spec("join", function () {
		o("join a single file", async function () {
			const ws: fs.WriteStream = mockWriteStream()
			const rs: fs.ReadStream = mockReadStream(ws)
			when(fs.createWriteStream(matchers.anything(), matchers.anything())).thenReturn(ws)
			when(fs.createReadStream(matchers.anything())).thenReturn(rs)
			// @ts-ignore callback omit
			when(rs.on("end")).thenCallback(undefined, undefined)
			when(fs.promises.readdir("/tutanota/tmp/path/unencrypted")).thenResolve(["folderContents"])
			when(tfs.ensureUnencrytpedDir()).thenResolve("/tutanota/tmp/path/unencrypted")
			const joinedFilePath = await ff.joinFiles("fileName.pdf", ["/file1"])
			o(joinedFilePath).equals("/tutanota/tmp/path/unencrypted/fileName.pdf")
		})
	})

	o.spec("splitFile", function () {
		o("returns one slice for a small file", async function () {
			// fs mock returns file name as the content
			const filename = "/tutanota/tmp/path/download/small.txt"
			const fileContent = stringToUtf8Uint8Array(filename)
			const filenameHash = "9ca089f82e397e9e860daa312ac25def39f2da0e066f0de94ffc02aa7b3a6250"
			const expectedChunkPath = `/tutanota/tmp/path/unencrypted/${filenameHash}.0.blob`
			when(tfs.ensureUnencrytpedDir()).thenResolve("/tutanota/tmp/path/unencrypted")
			when(fs.promises.writeFile(expectedChunkPath, fileContent)).thenResolve()
			when(fs.promises.readFile(filename)).thenResolve(Buffer.from(fileContent))
			const chunks = await ff.splitFile(filename, 1024)
			o(chunks).deepEquals([expectedChunkPath])("only one chunk")
		})

		o("returns multiple slices for a bigger file", async function () {
			// fs mock returns file name as the content
			const filename = "/tutanota/tmp/path/download/big.txt"
			// length 37
			const fileContent = stringToUtf8Uint8Array(filename)
			const filenameHash = "c24646a4738a92d624cd03134f26c371d8a2950d2b3bbce7921c288de9a56fd3"
			const expectedChunkPath0 = `/tutanota/tmp/path/unencrypted/${filenameHash}.0.blob`
			const expectedChunkPath1 = `/tutanota/tmp/path/unencrypted/${filenameHash}.1.blob`

			when(tfs.ensureUnencrytpedDir()).thenResolve("/tutanota/tmp/path/unencrypted")
			when(fs.promises.writeFile(expectedChunkPath0, fileContent.slice(0, 30))).thenResolve()
			when(fs.promises.writeFile(expectedChunkPath1, fileContent.slice(30))).thenResolve()
			when(fs.promises.readFile(filename)).thenResolve(Buffer.from(fileContent))
			const chunks = await ff.splitFile(filename, 30)
			o(chunks).deepEquals([expectedChunkPath0, expectedChunkPath1])("both written files are in the returned array")
		})
	})

	o.spec("showInFileExplorer", function () {
		o("two downloads, open two filemanagers", async function () {
			const dir = "/path/to"
			const p = dir + "/file.txt"
			await ff.showInFileExplorer(p)
			verify(electron.shell.showItemInFolder(p), { times: 1 })
		})

		o("two downloads, open two filemanagers after a pause", async function () {
			const time = 1629115820468
			const dir = "/path/to"
			const p = dir + "/file.txt"
			await ff.showInFileExplorer(p)
			when(dp.now()).thenReturn(time)
			when(conf.getConst(BuildConfigKey.fileManagerTimeout)).thenResolve(2)
			verify(electron.shell.showItemInFolder(p), { times: 1 })
			when(dp.now()).thenReturn(time + 10)
			await ff.showInFileExplorer(p)
			verify(electron.shell.showItemInFolder(p), { times: 2 })
		})
	})

	o.spec("putFileIntoDownloadsFolder", function () {
		o("putFileIntoDownloadsFolder", async function () {
			const src = "/path/random.pdf"
			const filename = "fileName.pdf"
			when(conf.getVar(DesktopConfigKey.defaultDownloadPath)).thenResolve(DEFAULT_DOWNLOAD_PATH)
			when(fs.promises.readdir(matchers.anything())).thenResolve([])
			const copiedFileUri = await ff.putFileIntoDownloadsFolder(src, filename)
			verify(fs.promises.copyFile(src, DEFAULT_DOWNLOAD_PATH + "fileName.pdf"))
			o(copiedFileUri).equals(DEFAULT_DOWNLOAD_PATH + "fileName.pdf")
		})
	})

	o.spec("size", function () {
		o("size", async function () {
			when(fs.promises.stat(matchers.anything())).thenResolve({ size: 33 })
			o(await ff.getSize("/file1")).equals(33)
		})
	})

	o.spec("hash", function () {
		o("hash", async function () {
			when(fs.promises.readFile("/file1")).thenResolve(new Uint8Array([0, 1, 2, 3]) as Buffer)
			o(await ff.hashFile("/file1")).equals("BU7ewdAh")
		})
	})
})

function mockReadStream(ws?: fs.WriteStream): fs.ReadStream {
	const rs: fs.ReadStream = object()
	if (ws != null) {
		when(rs.pipe(ws, { end: false })).thenReturn(ws)
	}

	return rs
}

const urlMatches = matchers.create({
	name: "urlMatches",
	matches(matcherArgs: any[], actual: any): boolean {
		return (actual as URL).toString() === (matcherArgs[0] as URL).toString()
	},
})

function mockWriteStream(response?: FetchResult): fs.WriteStream {
	const ws: fs.WriteStream = object()
	if (response != null) {
		// when(response.pipe(ws)).thenReturn(ws)
	}
	const closeCapturer = matchers.captor()
	when(ws.on("close", closeCapturer.capture())).thenReturn(ws)
	when(ws.close()).thenDo(() => closeCapturer.value())
	return ws
}

function mockResponse(
	statusCode: number,
	resOpts: {
		responseBody?: Uint8Array
		responseHeaders?: Record<string, string>
	},
): FetchResult {
	const { responseBody, responseHeaders } = resOpts
	return new global.Response(responseBody, {
		status: statusCode,
		headers: new Headers(responseHeaders),
	}) as FetchResult
}
