import {
	DoorLockCCCapabilitiesGet,
	DoorLockCCCapabilitiesReport,
	DoorLockCCConfigurationGet,
	DoorLockCCConfigurationReport,
	DoorLockCCConfigurationSet,
	DoorLockCCOperationGet,
	DoorLockCCOperationReport,
	DoorLockCCOperationSet,
	DoorLockCommand,
	DoorLockMode,
	DoorLockOperationType,
} from "@zwave-js/cc";
import { DoorLockCCValues } from "@zwave-js/cc/DoorLockCC";
import { CommandClasses, Duration } from "@zwave-js/core";
import { createTestingHost, TestingHost } from "@zwave-js/host";

function buildCCBuffer(payload: Buffer): Buffer {
	return Buffer.concat([
		Buffer.from([
			CommandClasses["Door Lock"], // CC
		]),
		payload,
	]);
}

describe("lib/commandclass/DoorLockCC => ", () => {
	let host: TestingHost;

	beforeAll(() => {
		host = createTestingHost();

		// Node 1 supports all Door Lock sensors
		const valueDB1 = host.getValueDB(1);
		valueDB1.setValue(DoorLockCCValues.doorSupported.id, true);
		valueDB1.setValue(DoorLockCCValues.boltSupported.id, true);
		valueDB1.setValue(DoorLockCCValues.latchSupported.id, true);

		// Node 2 doesn't support the door sensor
		const valueDB2 = host.getValueDB(2);
		valueDB2.setValue(DoorLockCCValues.doorSupported.id, false);
		valueDB2.setValue(DoorLockCCValues.boltSupported.id, true);
		valueDB2.setValue(DoorLockCCValues.latchSupported.id, true);
	});

	it("the OperationGet command should serialize correctly", () => {
		const cc = new DoorLockCCOperationGet(host, { nodeId: 1 });
		const expected = buildCCBuffer(
			Buffer.from([
				DoorLockCommand.OperationGet, // CC Command
			]),
		);
		expect(cc.serialize()).toEqual(expected);
	});

	it("the OperationSet command should serialize correctly", () => {
		const cc = new DoorLockCCOperationSet(host, {
			nodeId: 2,
			mode: DoorLockMode.OutsideUnsecured,
		});
		const expected = buildCCBuffer(
			Buffer.from([
				DoorLockCommand.OperationSet, // CC Command
				0x20, // target value
			]),
		);
		expect(cc.serialize()).toEqual(expected);
	});

	it("the OperationReport command (v1-v3) should be deserialized correctly", () => {
		const ccData = buildCCBuffer(
			Buffer.from([
				DoorLockCommand.OperationReport, // CC Command
				DoorLockMode.InsideUnsecuredWithTimeout, // lock mode
				0b1000_0010, // handles mode
				0b010, // condition
				50, // timeout minutes
				20, // timeout seconds
			]),
		);
		const cc = new DoorLockCCOperationReport(host, {
			nodeId: 1,
			data: ccData,
		});

		expect(cc.currentMode).toBe(DoorLockMode.InsideUnsecuredWithTimeout);
		expect(cc.outsideHandlesCanOpenDoor).toEqual([
			false,
			false,
			false,
			true,
		]);
		expect(cc.insideHandlesCanOpenDoor).toEqual([
			false,
			true,
			false,
			false,
		]);
		expect(cc.lockTimeout).toBe(50 * 60 + 20);
		expect(cc.latchStatus).toBe("open");
		expect(cc.boltStatus).toBe("unlocked");
		expect(cc.doorStatus).toBe("open");
		expect(cc.targetMode).toBeUndefined();
		expect(cc.duration).toBeUndefined();
	});

	it("the OperationReport command (v4) should be deserialized correctly", () => {
		const ccData = buildCCBuffer(
			Buffer.from([
				DoorLockCommand.OperationReport, // CC Command
				DoorLockMode.OutsideUnsecured, // lock mode
				0b0100_1111, // handles mode
				0b101, // condition
				0x00, // timeout minutes
				0xfe, // invalid timeout seconds
				DoorLockMode.Secured,
				0x01, // 1 second left
			]),
		);
		const cc = new DoorLockCCOperationReport(host, {
			nodeId: 2,
			data: ccData,
		});
		cc.persistValues(host);

		expect(cc.currentMode).toBe(DoorLockMode.OutsideUnsecured);
		expect(cc.outsideHandlesCanOpenDoor).toEqual([
			false,
			false,
			true,
			false,
		]);
		expect(cc.insideHandlesCanOpenDoor).toEqual([true, true, true, true]);
		expect(cc.lockTimeout).toBeUndefined();
		expect(cc.latchStatus).toBe("closed");
		expect(cc.boltStatus).toBe("locked");
		// The CC itself contains the door status, but it does not get persisted (unsupported)
		expect(cc.doorStatus).toBe("closed");
		expect(
			host
				.getValueDB(cc.nodeId as number)
				.getValue(
					DoorLockCCValues.doorStatus.endpoint(cc.endpointIndex),
				),
		).toBe(undefined);
		expect(cc.targetMode).toBe(DoorLockMode.Secured);
		expect(cc.duration).toEqual(new Duration(1, "seconds"));
	});

	it("the ConfigurationGet command should serialize correctly", () => {
		const cc = new DoorLockCCConfigurationGet(host, { nodeId: 1 });
		const expected = buildCCBuffer(
			Buffer.from([
				DoorLockCommand.ConfigurationGet, // CC Command
			]),
		);
		expect(cc.serialize()).toEqual(expected);
	});

	it("the ConfigurationReport command (v1-v3) should be deserialized correctly", () => {
		const ccData = buildCCBuffer(
			Buffer.from([
				DoorLockCommand.ConfigurationReport, // CC Command
				DoorLockOperationType.Timed, // operation type
				0b1000_0010, // handles mode
				50, // timeout minutes
				20, // timeout seconds
			]),
		);
		const cc = new DoorLockCCConfigurationReport(host, {
			nodeId: 1,
			data: ccData,
		});

		expect(cc.operationType).toBe(DoorLockOperationType.Timed);
		expect(cc.outsideHandlesCanOpenDoorConfiguration).toEqual([
			false,
			false,
			false,
			true,
		]);
		expect(cc.insideHandlesCanOpenDoorConfiguration).toEqual([
			false,
			true,
			false,
			false,
		]);
		expect(cc.lockTimeoutConfiguration).toBe(50 * 60 + 20);
		expect(cc.autoRelockTime).toBeUndefined();
		expect(cc.holdAndReleaseTime).toBeUndefined();
		expect(cc.twistAssist).toBeUndefined();
		expect(cc.blockToBlock).toBeUndefined();
	});

	it("the ConfigurationReport command must ignore invalid timeouts (constant)", () => {
		const ccData = buildCCBuffer(
			Buffer.from([
				DoorLockCommand.ConfigurationReport, // CC Command
				DoorLockOperationType.Constant, // operation type
				0b1000_0010, // handles mode
				50, // timeout minutes
				20, // timeout seconds
			]),
		);
		const cc = new DoorLockCCConfigurationReport(host, {
			nodeId: 1,
			data: ccData,
		});

		expect(cc.lockTimeoutConfiguration).toBeUndefined();
	});

	it("the ConfigurationReport command must ignore invalid timeouts (invalid minutes)", () => {
		const ccData = buildCCBuffer(
			Buffer.from([
				DoorLockCommand.ConfigurationReport, // CC Command
				DoorLockOperationType.Constant, // operation type
				0b1000_0010, // handles mode
				0xff, // timeout minutes
				20, // timeout seconds
			]),
		);
		const cc = new DoorLockCCConfigurationReport(host, {
			nodeId: 1,
			data: ccData,
		});

		expect(cc.lockTimeoutConfiguration).toBeUndefined();
	});

	it("the ConfigurationReport command must ignore invalid timeouts (invalid seconds)", () => {
		const ccData = buildCCBuffer(
			Buffer.from([
				DoorLockCommand.ConfigurationReport, // CC Command
				DoorLockOperationType.Constant, // operation type
				0b1000_0010, // handles mode
				20, // timeout minutes
				0xff, // timeout seconds
			]),
		);
		const cc = new DoorLockCCConfigurationReport(host, {
			nodeId: 1,
			data: ccData,
		});

		expect(cc.lockTimeoutConfiguration).toBeUndefined();
	});

	it("the ConfigurationReport command (v4) should be deserialized correctly", () => {
		const ccData = buildCCBuffer(
			Buffer.from([
				DoorLockCommand.ConfigurationReport, // CC Command
				DoorLockOperationType.Timed, // operation type
				0b1000_0010, // handles mode
				50, // timeout minutes
				20, // timeout seconds

				0xff, // auto relock
				0x01,
				0x02, // hold and release
				0x03,
				0b01, // flags
			]),
		);
		const cc = new DoorLockCCConfigurationReport(host, {
			nodeId: 1,
			data: ccData,
		});

		expect(cc.autoRelockTime).toBe(0xff01);
		expect(cc.holdAndReleaseTime).toBe(0x0203);
		expect(cc.twistAssist).toBeTrue();
		expect(cc.blockToBlock).toBeFalse();
	});

	it("the ConfigurationSet command (v1-3) should serialize correctly (timed)", () => {
		const cc = new DoorLockCCConfigurationSet(host, {
			nodeId: 2,
			operationType: DoorLockOperationType.Timed,
			outsideHandlesCanOpenDoorConfiguration: [false, true, true, true],
			insideHandlesCanOpenDoorConfiguration: [true, false, true, false],
			lockTimeoutConfiguration: 60 * 15 + 18,
		});
		const expected = buildCCBuffer(
			Buffer.from([
				DoorLockCommand.ConfigurationSet, // CC Command
				DoorLockOperationType.Timed,
				0b1110_0101,
				15,
				18,
			]),
		);
		expect(cc.serialize()).toEqual(expected);
	});

	it("the ConfigurationSet command (v1-3) should serialize correctly (constant)", () => {
		const cc = new DoorLockCCConfigurationSet(host, {
			nodeId: 2,
			operationType: DoorLockOperationType.Constant,
			outsideHandlesCanOpenDoorConfiguration: [false, true, true, true],
			insideHandlesCanOpenDoorConfiguration: [true, false, true, false],
		});
		const expected = buildCCBuffer(
			Buffer.from([
				DoorLockCommand.ConfigurationSet, // CC Command
				DoorLockOperationType.Constant,
				0b1110_0101,
				0xfe,
				0xfe,
			]),
		);
		expect(cc.serialize()).toEqual(expected);
	});

	it("the ConfigurationSet command (v4) should serialize correctly", () => {
		const cc = new DoorLockCCConfigurationSet(host, {
			nodeId: 2,
			operationType: DoorLockOperationType.Timed,
			outsideHandlesCanOpenDoorConfiguration: [false, true, true, true],
			insideHandlesCanOpenDoorConfiguration: [true, false, true, false],
			lockTimeoutConfiguration: 60 * 15 + 18,
			autoRelockTime: 0xfefd,
			holdAndReleaseTime: undefined,
			blockToBlock: undefined,
			twistAssist: true,
		});
		const expected = buildCCBuffer(
			Buffer.from([
				DoorLockCommand.ConfigurationSet, // CC Command
				DoorLockOperationType.Timed,
				0b1110_0101,
				15,
				18,
				0xfe,
				0xfd,
				0,
				0,
				0b1,
			]),
		);
		expect(cc.serialize()).toEqual(expected);
	});

	it("the CapabilitiesGet command should serialize correctly", () => {
		const cc = new DoorLockCCCapabilitiesGet(host, { nodeId: 1 });
		const expected = buildCCBuffer(
			Buffer.from([
				DoorLockCommand.CapabilitiesGet, // CC Command
			]),
		);
		expect(cc.serialize()).toEqual(expected);
	});

	it("the CapabilitiesReport command should be deserialized correctly", () => {
		const ccData = buildCCBuffer(
			Buffer.from([
				DoorLockCommand.CapabilitiesReport, // CC Command
				1, // bit mask length
				0b110, // operation types
				3, // list length
				DoorLockMode.Unsecured,
				DoorLockMode.InsideUnsecured,
				DoorLockMode.Secured,
				0b0000_0000, // handle modes
				0b111, // components
				0b1010, // feature flags
			]),
		);
		const cc = new DoorLockCCCapabilitiesReport(host, {
			nodeId: 1,
			data: ccData,
		});

		expect(cc.supportedOperationTypes).toEqual([
			DoorLockOperationType.Constant,
			DoorLockOperationType.Timed,
		]);
		expect(cc.supportedDoorLockModes).toEqual([
			DoorLockMode.Unsecured,
			DoorLockMode.InsideUnsecured,
			DoorLockMode.Secured,
		]);
		expect(cc.latchSupported).toBeTrue();
		expect(cc.boltSupported).toBeTrue();
		expect(cc.doorSupported).toBeTrue();

		expect(cc.autoRelockSupported).toBeTrue();
		expect(cc.holdAndReleaseSupported).toBeFalse();
		expect(cc.twistAssistSupported).toBeTrue();
		expect(cc.blockToBlockSupported).toBeFalse();
	});

	// it("the Report command (v2) should be deserialized correctly", () => {
	// 	const ccData = buildCCBuffer(
	// 		1,
	// 		Buffer.from([
	// 			DoorLockCommand.Report, // CC Command
	// 			55, // current value
	// 			66, // target value
	// 			1, // duration
	// 		]),
	// 	);
	// 	const cc = new DoorLockCCReport(host, { data: ccData });

	// 	expect(cc.currentValue).toBe(55);
	// 	expect(cc.targetValue).toBe(66);
	// 	expect(cc.duration!.unit).toBe("seconds");
	// 	expect(cc.duration!.value).toBe(1);
	// });

	// it("deserializing an unsupported command should return an unspecified version of DoorLockCC", () => {
	// 	const serializedCC = buildCCBuffer(
	// 		1,
	// 		Buffer.from([255]), // not a valid command
	// 	);
	// 	const cc: any = new DoorLockCC(host, {
	// 		data: serializedCC,
	// 	});
	// 	expect(cc.constructor).toBe(DoorLockCC);
	// });

	// it("the CC values should have the correct metadata", () => {
	// 	// Readonly, 0-99
	// 	const currentValueMeta = getCCValueMetadata(
	// 		CommandClasses.DoorLock,
	// 		"currentValue",
	// 	);
	// 	expect(currentValueMeta).toMatchObject({
	// 		readable: true,
	// 		writeable: false,
	// 		min: 0,
	// 		max: 99,
	// 	});

	// 	// Writeable, 0-99
	// 	const targetValueMeta = getCCValueMetadata(
	// 		CommandClasses.DoorLock,
	// 		"targetValue",
	// 	);
	// 	expect(targetValueMeta).toMatchObject({
	// 		readable: true,
	// 		writeable: true,
	// 		min: 0,
	// 		max: 99,
	// 	});
	// });
});
