import fs from "node:fs/promises";
import os from "node:os";
import { afterEach, describe, expect, test } from "vitest";
import { buildEnv } from "../build-env";
import type { ResticDeps } from "../../types";

const RESTIC_CACHE_DIR = "/tmp/restic-cache";
const RESTIC_PASS_FILE = "/tmp/restic.pass";

const makeDeps = (overrides: Partial<ResticDeps> = {}): ResticDeps => ({
	resolveSecret: async (s) => s,
	getOrganizationResticPassword: async () => "org-restic-password",
	resticCacheDir: RESTIC_CACHE_DIR,
	resticPassFile: RESTIC_PASS_FILE,
	defaultExcludes: [RESTIC_PASS_FILE, "/var/lib/zerobyte/repositories"],
	rcloneConfigFile: "/tmp/rclone.conf",
	...overrides,
});

const withCustomPassword = <T extends object>(config: T) => ({
	...config,
	isExistingRepository: true as const,
	customPassword: "test-password",
});

const PLAIN_PRIVATE_KEY =
	"-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAA\n-----END OPENSSH PRIVATE KEY-----";
const tempDirs = new Set<string>();

const buildEnvForTest = async (
	config: Parameters<typeof buildEnv>[0],
	organizationId: string,
	deps: ResticDeps = makeDeps(),
) => {
	const env = await buildEnv(config, organizationId, deps);
	if (env._RUNTIME_SECRETS_DIR) tempDirs.add(env._RUNTIME_SECRETS_DIR);
	return env;
};

const listResticTempDirs = async () => {
	return new Set((await fs.readdir(os.tmpdir())).filter((name) => name.startsWith("zerobyte-restic-")));
};

afterEach(async () => {
	await Promise.all([...tempDirs].map((dir) => fs.rm(dir, { recursive: true, force: true })));
	tempDirs.clear();
});

describe("buildEnv", () => {
	describe("base environment", () => {
		test("always sets RESTIC_CACHE_DIR", async () => {
			const env = await buildEnvForTest(
				withCustomPassword({ backend: "local" as const, path: "/tmp/repo" }),
				"org-1",
			);

			expect(env.RESTIC_CACHE_DIR).toBe(RESTIC_CACHE_DIR);
		});

		test("always sets PATH", async () => {
			const env = await buildEnvForTest(
				withCustomPassword({ backend: "local" as const, path: "/tmp/repo" }),
				"org-1",
			);

			expect(env.PATH).toBeTruthy();
		});
	});

	describe("password resolution", () => {
		test("writes a password file when using customPassword on an existing repository", async () => {
			const env = await buildEnvForTest(
				{
					backend: "local" as const,
					path: "/tmp/repo",
					isExistingRepository: true,
					customPassword: "my-secret",
				},
				"org-1",
			);

			const passwordFilePath = env.RESTIC_PASSWORD_FILE;
			expect(passwordFilePath).toBeDefined();
			if (!passwordFilePath) {
				throw new Error("Expected password file path to be defined");
			}

			const fileContent = await fs.readFile(passwordFilePath, "utf-8");
			expect(fileContent).toBe("my-secret");
		});

		test("writes a password file from the organization's resticPassword when no customPassword is given", async () => {
			const deps = makeDeps({
				getOrganizationResticPassword: async () => "org-restic-password",
			});

			const env = await buildEnvForTest({ backend: "local" as const, path: "/tmp/repo" }, "org-1", deps);

			const passwordFilePath = env.RESTIC_PASSWORD_FILE;
			expect(passwordFilePath).toBeDefined();
			if (!passwordFilePath) {
				throw new Error("Expected password file path to be defined");
			}

			const fileContent = await fs.readFile(passwordFilePath, "utf-8");
			expect(fileContent).toBe("org-restic-password");
		});

		test("throws when getOrganizationResticPassword throws (organization not found)", async () => {
			const deps = makeDeps({
				getOrganizationResticPassword: async () => {
					throw new Error("Organization non-existent-org not found");
				},
			});
			const before = await listResticTempDirs();

			await expect(
				buildEnv({ backend: "local" as const, path: "/tmp/repo" }, "non-existent-org", deps),
			).rejects.toThrow("Organization non-existent-org not found");

			expect(await listResticTempDirs()).toEqual(before);
		});

		test("cleans the temp directory when setup fails after creating one", async () => {
			const before = await listResticTempDirs();
			const deps = makeDeps({
				resolveSecret: async (secret) => {
					if (secret === "my-access-key") throw new Error("failed to resolve access key");
					return secret;
				},
			});

			await expect(
				buildEnv(
					withCustomPassword({
						backend: "s3" as const,
						endpoint: "https://s3.amazonaws.com",
						bucket: "my-bucket",
						accessKeyId: "my-access-key",
						secretAccessKey: "my-secret-key",
					}),
					"org-1",
					deps,
				),
			).rejects.toThrow("failed to resolve access key");

			expect(await listResticTempDirs()).toEqual(before);
		});

		test("throws when getOrganizationResticPassword throws (no password configured)", async () => {
			const deps = makeDeps({
				getOrganizationResticPassword: async (id) => {
					throw new Error(`Restic password not configured for organization ${id}`);
				},
			});

			await expect(
				buildEnv({ backend: "local" as const, path: "/tmp/repo" }, "org-no-pass", deps),
			).rejects.toThrow("Restic password not configured for organization org-no-pass");
		});
	});

	describe("s3 backend", () => {
		const base = withCustomPassword({
			backend: "s3" as const,
			endpoint: "https://s3.amazonaws.com",
			bucket: "my-bucket",
			accessKeyId: "my-access-key",
			secretAccessKey: "my-secret-key",
		});

		test("sets AWS credentials", async () => {
			const env = await buildEnvForTest(base, "org-1");

			expect(env.AWS_ACCESS_KEY_ID).toBe("my-access-key");
			expect(env.AWS_SECRET_ACCESS_KEY).toBe("my-secret-key");
		});

		test("sets AWS_S3_BUCKET_LOOKUP=dns for Huawei Cloud endpoints", async () => {
			const env = await buildEnvForTest(
				{ ...base, endpoint: "https://obs.ap-southeast-1.myhuaweicloud.com" },
				"org-1",
			);

			expect(env.AWS_S3_BUCKET_LOOKUP).toBe("dns");
		});

		test("does not set AWS_S3_BUCKET_LOOKUP for standard S3 endpoints", async () => {
			const env = await buildEnvForTest(base, "org-1");

			expect(env.AWS_S3_BUCKET_LOOKUP).toBeUndefined();
		});
	});

	describe("r2 backend", () => {
		test("sets AWS credentials with auto region and forced path style", async () => {
			const env = await buildEnvForTest(
				withCustomPassword({
					backend: "r2" as const,
					endpoint: "https://myaccount.r2.cloudflarestorage.com",
					bucket: "my-bucket",
					accessKeyId: "r2-access-key",
					secretAccessKey: "r2-secret-key",
				}),
				"org-1",
			);

			expect(env.AWS_ACCESS_KEY_ID).toBe("r2-access-key");
			expect(env.AWS_SECRET_ACCESS_KEY).toBe("r2-secret-key");
			expect(env.AWS_REGION).toBe("auto");
			expect(env.AWS_S3_FORCE_PATH_STYLE).toBe("true");
		});
	});

	describe("gcs backend", () => {
		test("sets project ID and writes credentials file", async () => {
			const env = await buildEnvForTest(
				withCustomPassword({
					backend: "gcs" as const,
					bucket: "my-gcs-bucket",
					projectId: "my-gcp-project",
					credentialsJson: '{"type":"service_account"}',
				}),
				"org-1",
			);

			expect(env.GOOGLE_PROJECT_ID).toBe("my-gcp-project");

			const credentialsPath = env.GOOGLE_APPLICATION_CREDENTIALS;
			expect(credentialsPath).toBeDefined();
			if (!credentialsPath) {
				throw new Error("Expected credentials path to be defined");
			}

			const fileContent = await fs.readFile(credentialsPath, "utf-8");
			expect(fileContent).toBe('{"type":"service_account"}');
		});
	});

	describe("azure backend", () => {
		const base = withCustomPassword({
			backend: "azure" as const,
			container: "my-container",
			accountName: "mystorageaccount",
			accountKey: "my-account-key",
		});

		test("sets account name and key", async () => {
			const env = await buildEnvForTest(base, "org-1");

			expect(env.AZURE_ACCOUNT_NAME).toBe("mystorageaccount");
			expect(env.AZURE_ACCOUNT_KEY).toBe("my-account-key");
		});

		test("includes endpoint suffix when provided", async () => {
			const env = await buildEnvForTest({ ...base, endpointSuffix: "core.chinacloudapi.cn" }, "org-1");

			expect(env.AZURE_ENDPOINT_SUFFIX).toBe("core.chinacloudapi.cn");
		});

		test("omits endpoint suffix when not provided", async () => {
			const env = await buildEnvForTest(base, "org-1");

			expect(env.AZURE_ENDPOINT_SUFFIX).toBeUndefined();
		});
	});

	describe("rest backend", () => {
		test("sets username and password when both are provided", async () => {
			const env = await buildEnvForTest(
				withCustomPassword({
					backend: "rest" as const,
					url: "https://rest-server.example.com",
					username: "rest-user",
					password: "rest-pass",
				}),
				"org-1",
			);

			expect(env.RESTIC_REST_USERNAME).toBe("rest-user");
			expect(env.RESTIC_REST_PASSWORD).toBe("rest-pass");
		});

		test("omits REST credentials when neither username nor password is provided", async () => {
			const env = await buildEnvForTest(
				withCustomPassword({ backend: "rest" as const, url: "https://rest-server.example.com" }),
				"org-1",
			);

			expect(env.RESTIC_REST_USERNAME).toBeUndefined();
			expect(env.RESTIC_REST_PASSWORD).toBeUndefined();
		});
	});

	describe("rclone backend", () => {
		test("sets RCLONE_NO_CHECK_CERTIFICATE when insecureTls is enabled", async () => {
			const env = await buildEnvForTest(
				withCustomPassword({
					backend: "rclone" as const,
					remote: "my-remote",
					path: "/backups",
					insecureTls: true,
				}),
				"org-1",
			);

			expect(env._INSECURE_TLS).toBe("true");
			expect(env.RCLONE_NO_CHECK_CERTIFICATE).toBe("true");
		});
	});

	describe("sftp backend", () => {
		const baseSftpConfig = withCustomPassword({
			backend: "sftp" as const,
			host: "backup.example.com",
			port: 22,
			user: "backup",
			path: "/backups",
			privateKey: PLAIN_PRIVATE_KEY,
			skipHostKeyCheck: true as const,
			allowLegacySshRsa: false as const,
		});

		test("throws for passphrase-protected private keys", async () => {
			const deps = makeDeps();
			await expect(
				buildEnv(
					{
						...baseSftpConfig,
						privateKey:
							"-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\n-----END RSA PRIVATE KEY-----",
					},
					"org-1",
					deps,
				),
			).rejects.toThrow("Passphrase-protected SSH keys are not supported");
		});

		test("succeeds when the private key has CRLF line endings", async () => {
			const crlfKey = PLAIN_PRIVATE_KEY.replace(/\n/g, "\r\n");
			const deps = makeDeps();

			await expect(buildEnv({ ...baseSftpConfig, privateKey: crlfKey }, "org-1", deps)).resolves.toBeDefined();
		});

		test("uses StrictHostKeyChecking=no when skipHostKeyCheck is true", async () => {
			const env = await buildEnvForTest({ ...baseSftpConfig, skipHostKeyCheck: true }, "org-1");

			expect(env._SFTP_SSH_ARGS).toContain("StrictHostKeyChecking=no");
			expect(env._SFTP_SSH_ARGS).toContain("UserKnownHostsFile=/dev/null");
		});

		test("does not allow legacy RSA/SHA1 SSH algorithms by default", async () => {
			const env = await buildEnvForTest(baseSftpConfig, "org-1");

			expect(env._SFTP_SSH_ARGS).not.toContain("HostKeyAlgorithms=+ssh-rsa");
			expect(env._SFTP_SSH_ARGS).not.toContain("PubkeyAcceptedAlgorithms=+ssh-rsa");
		});

		test("allows legacy RSA/SHA1 SSH algorithms after modern defaults when enabled", async () => {
			const env = await buildEnvForTest({ ...baseSftpConfig, allowLegacySshRsa: true }, "org-1");

			expect(env._SFTP_SSH_ARGS).toContain("HostKeyAlgorithms=+ssh-rsa");
			expect(env._SFTP_SSH_ARGS).toContain("PubkeyAcceptedAlgorithms=+ssh-rsa");
		});

		test("uses StrictHostKeyChecking=yes when knownHosts is absent", async () => {
			const env = await buildEnvForTest(
				{ ...baseSftpConfig, skipHostKeyCheck: false, knownHosts: undefined },
				"org-1",
			);

			expect(env._SFTP_SSH_ARGS).toContain("StrictHostKeyChecking=yes");
			expect(env._SFTP_SSH_ARGS).not.toContain("UserKnownHostsFile=/dev/null");
		});

		test("uses StrictHostKeyChecking=yes and a known hosts file when knownHosts is provided", async () => {
			const env = await buildEnvForTest(
				{
					...baseSftpConfig,
					skipHostKeyCheck: false,
					knownHosts: "backup.example.com ssh-rsa AAAAB3NzaC1...",
				},
				"org-1",
			);

			expect(env._SFTP_SSH_ARGS).toContain("StrictHostKeyChecking=yes");
			expect(env._SFTP_SSH_ARGS).toContain("/zerobyte-known-hosts-");
			expect(env._SFTP_KNOWN_HOSTS_PATH).toMatch(/\/zerobyte-restic-[^/]+\/zerobyte-known-hosts-/);
		});

		test("adds -p flag for non-default ports", async () => {
			const env = await buildEnvForTest({ ...baseSftpConfig, port: 2222 }, "org-1");

			expect(env._SFTP_SSH_ARGS).toContain("-p 2222");
		});

		test("omits -p flag for the default port 22", async () => {
			const env = await buildEnvForTest({ ...baseSftpConfig, port: 22 }, "org-1");

			expect(env._SFTP_SSH_ARGS).not.toContain("-p 22");
		});

		test("sets the key path in both _SFTP_KEY_PATH and SSH args -i flag", async () => {
			const env = await buildEnvForTest(baseSftpConfig, "org-1");

			expect(env._SFTP_KEY_PATH).toMatch(/\/zerobyte-restic-[^/]+\/zerobyte-ssh-/);
			expect(env._SFTP_SSH_ARGS).toContain(`-i ${env._SFTP_KEY_PATH}`);
		});
	});

	describe("cacert", () => {
		test("sets RESTIC_CACERT pointing to a temp file when cacert is provided", async () => {
			const env = await buildEnvForTest(
				withCustomPassword({
					backend: "local" as const,
					path: "/tmp/repo",
					cacert: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",
				}),
				"org-1",
			);

			const certPath = env.RESTIC_CACERT;
			expect(certPath).toBeDefined();
			if (!certPath) {
				throw new Error("Expected certificate path to be defined");
			}

			const fileContent = await fs.readFile(certPath, "utf-8");
			expect(fileContent).toBe("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----");
		});

		test("does not set RESTIC_CACERT when cacert is absent", async () => {
			const env = await buildEnvForTest(
				withCustomPassword({ backend: "local" as const, path: "/tmp/repo" }),
				"org-1",
			);

			expect(env.RESTIC_CACERT).toBeUndefined();
		});
	});

	describe("insecure TLS", () => {
		test("sets _INSECURE_TLS=true when insecureTls is true", async () => {
			const env = await buildEnvForTest(
				withCustomPassword({ backend: "local" as const, path: "/tmp/repo", insecureTls: true }),
				"org-1",
			);

			expect(env._INSECURE_TLS).toBe("true");
		});

		test("does not set _INSECURE_TLS when insecureTls is absent", async () => {
			const env = await buildEnvForTest(
				withCustomPassword({ backend: "local" as const, path: "/tmp/repo" }),
				"org-1",
			);

			expect(env._INSECURE_TLS).toBeUndefined();
		});
	});
});
