import {
  Configuration,
  Logger as FahLogger,
  SystemAccessPoint,
  WebSocketMessage,
} from "freeathome-local-api-client";
import { Logger, PlatformAccessory, PlatformConfig } from "homebridge";
import { HomebridgeAPI } from "homebridge/api";
import { Subject, Subscription } from "rxjs";
import { FreeAtHomeAccessory } from "../src/freeAtHomeAccessory.js";
import {
  FreeAtHomeContext,
  isFreeAtHomeAccessory,
} from "../src/freeAtHomeContext.js";
import { FreeAtHomeHomebridgePlatform } from "../src/platform.js";
import { SwitchActuatorAccessory } from "../src/switchActuatorAccessory.js";
import { AccessoryType } from "../src/typeMappings.js";
import { TestAccessory } from "./TestAccessory.mock.js";
import { createPlatformAccessory } from "./platform.mock.js";

const configuration: Configuration = {
  "00000000-0000-0000-0000-000000000000": {
    sysapName: "Gerke",
    devices: {
      EXCLUDED0001: {
        floor: "1",
        room: "1",
        channels: {
          ch0001: {
            functionID: "7d",
          },
        },
      },
      FFFF48000001: {},
      ABB700000000: {},
      ABB700000001: {
        channels: {
          ch0000: {},
          ch0001: {
            functionID: "never",
          },
          ch0002: {
            functionID: "7",
          },
          ch0003: {
            functionID: "7",
            floor: "1",
            room: "1",
          },
          ch0004: {
            displayName: "Dimmer",
            functionID: "12",
            floor: "1",
            room: "1",
            outputs: {
              odp0000: {
                pairingID: 256,
              },
              odp0001: {
                pairingID: 272,
              },
            },
            inputs: {
              idp0000: {
                pairingID: 1,
              },
              idp0001: {
                pairingID: 17,
              },
            },
          },
        },
      },
      ABB700000002: {
        floor: "1",
        room: "1",
        channels: {
          ch0000: {
            functionID: "23",
            outputs: {
              odp0000: {
                pairingID: 48,
              },
              odp0001: {
                pairingID: 50,
              },
              odp0006: {
                pairingID: 51,
              },
              odp0008: {
                pairingID: 56,
              },
              odp0010: {
                pairingID: 304,
              },
            },
            inputs: {
              idp0012: { pairingID: 66 },
              idp0016: { pairingID: 320 },
            },
          },
          ch0001: {
            functionID: "20",
          },
          ch0002: {
            functionID: "7D",
          },
          ch0003: {
            functionID: "11",
            outputs: {
              odp0123: {
                pairingID: 6,
              },
            },
          },
          ch0004: {
            functionID: "1a",
          },
          ch0005: {
            functionID: "1a",
          },
        },
      },
      ABB700000003: {
        channels: {
          ch0003: {
            functionID: "7",
            floor: "1",
            room: "1",
          },
          ch0004: {
            displayName: "Dimmer",
            functionID: "12",
            floor: "1",
            room: "1",
            outputs: {
              odp0000: {
                pairingID: 256,
              },
              odp0001: {
                pairingID: 272,
              },
            },
            inputs: {
              idp0000: {
                pairingID: 1,
              },
              idp0001: {
                pairingID: 17,
              },
            },
          },
          ch0005: {
            functionID: "9",
          },
        },
      },
      ABB700000004: {
        floor: "1",
        room: "1",
        channels: {
          ch0005: {
            functionID: "9",
          },
        },
      },
      ABB700000005: {
        floor: "1",
        room: "1",
        channels: {
          ch0001: {
            functionID: "7",
          },
          ch0002: {
            functionID: "4800",
          },
          ch0006: {
            functionID: "3e",
            outputs: {
              odp0000: {
                pairingID: 331,
              },
              odp0006: {
                pairingID: 51,
              },
              odp0008: {
                pairingID: 56,
              },
              odp0010: {
                pairingID: 304,
              },
            },
            inputs: {
              idp0012: { pairingID: 66 },
              idp0016: { pairingID: 320 },
            },
          },
        },
      },
      ABB700000006: {
        floor: "1",
        room: "1",
        channels: {
          ch0001: {
            functionID: "0",
          },
          ch0002: {
            functionID: "F",
            outputs: {
              dp0: {
                pairingID: 53,
              },
            },
          },
          ch0003: {
            functionID: "6",
            outputs: {
              dp0: {
                pairingID: 4,
              },
            },
          },
          ch0004: {
            functionID: "4",
            outputs: {
              dp0: {
                pairingID: 2,
              },
            },
          },
          ch0005: {
            functionID: "45",
            inputs: {
              dp0: {
                pairingID: 2,
              },
            },
          },
          ch0006: {
            functionID: "43",
            outputs: {
              dp0: {
                pairingID: 1024,
              },
            },
          },
          ch0007: {
            functionID: "41",
            outputs: {
              dp0: {
                pairingID: 1027,
              },
            },
          },
          ch0008: {
            functionID: "5E",
            outputs: {
              dp0: {
                pairingID: 2,
              },
            },
          },
        },
      },
      E11000000001: {
        floor: "1",
        room: "1",
        channels: {
          ch0001: {
            functionID: "7d",
          },
        },
      },
    },
    floorplan: {
      floors: {},
    },
    users: {},
  },
};

describe("free@home Homebridge Platform", () => {
  let api: HomebridgeAPI;
  let config: PlatformConfig;
  let logger: Logger;

  beforeEach(() => {
    api = new HomebridgeAPI();
    config = {
      name: "Test Platform Configuration",
      platform: "free@home Unit Testing Platform",
      ignoredChannels: ["ABB700000002/ch0005", "ABB700000003/*"],
    };
    logger = {
      debug: jasmine.createSpy(),
      error: jasmine.createSpy(),
      info: jasmine.createSpy(),
      warn: jasmine.createSpy(),
      log: jasmine.createSpy(),
      success: jasmine.createSpy(),
    };
  });

  it("should be created", () => {
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    expect(platform).toBeTruthy();
  });

  it("should handle API lifecycle events", () => {
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    const instance = platform as unknown as {
      webSocketSubscription: Subscription;
      discoverDevices: () => Promise<void>;
    };
    const spyWsConnect = spyOn(platform.sysap, "connectWebSocket");
    const spyUnsubscribe = spyOn(instance.webSocketSubscription, "unsubscribe");
    const spyDiscovery = spyOn(instance, "discoverDevices").and.resolveTo();
    api.emit("didFinishLaunching");
    expect(spyWsConnect).toHaveBeenCalled();
    expect(spyDiscovery).toHaveBeenCalled();
    api.emit("shutdown");
    expect(spyUnsubscribe).toHaveBeenCalled();
  });

  it("should handle device discovery failure", () => {
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    const instance = platform as unknown as {
      discoverDevices: () => Promise<void>;
    };
    spyOn(platform.sysap, "connectWebSocket");
    const spyDiscovery = spyOn(instance, "discoverDevices").and.rejectWith(
      "Test Error"
    );
    api.emit("didFinishLaunching");
    expect(spyDiscovery).toHaveBeenCalled();
  });

  it("should reset connection attempt count when websocket is connected", () => {
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    const instance = platform as unknown as {
      wsConnectionAttempt: number;
      sysap: SystemAccessPoint;
    };
    instance.wsConnectionAttempt = 3;
    instance.sysap.emit("websocket-open");
    expect(instance.wsConnectionAttempt).toBe(0);
  });

  it("should handle expected websocket closure", () => {
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    const spy = spyOn(platform.sysap, "connectWebSocket");
    platform.sysap.emit("websocket-close", 1000, Buffer.from("Bye"));
    expect(spy).not.toHaveBeenCalled();
  });

  it("should not attempt to reconnect after reconnection attempt limit was reached", () => {
    config.maxWsRetryCount = 3;
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    const instance = platform as unknown as {
      wsConnectionAttempt: number;
    };
    instance.wsConnectionAttempt = 3;
    const spy = spyOn(platform.sysap, "connectWebSocket");
    platform.sysap.emit(
      "websocket-close",
      1234,
      Buffer.from("Disconnect Test")
    );
    expect(spy).not.toHaveBeenCalled();
  });

  it("should attempt to reconnect if reconnection attempt limit is not reached", () => {
    jasmine.clock().install();
    config.maxWsRetryCount = 3;
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    const instance = platform as unknown as {
      wsConnectionAttempt: number;
    };
    const spy = spyOn(platform.sysap, "connectWebSocket");
    platform.sysap.emit(
      "websocket-close",
      1234,
      Buffer.from("Disconnect Test")
    );
    jasmine.clock().tick(230);
    expect(spy).toHaveBeenCalled();
    expect(instance.wsConnectionAttempt).toBe(1);
    jasmine.clock().uninstall();
  });

  it("should receive message from SysAP websocket", () => {
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    const instance = platform as unknown as {
      readonly sysap: {
        readonly webSocketMessageSubject: Subject<WebSocketMessage>;
      };
      processWebSocketMesage: (message: WebSocketMessage) => void;
    };
    const spy = spyOn(instance, "processWebSocketMesage");
    instance.sysap.webSocketMessageSubject.next({});
    expect(spy).toHaveBeenCalledWith({});
  });

  it("should forward log messages from SysAP to Homebridge logger", () => {
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    const instance = platform as unknown as {
      readonly sysap: {
        readonly logger: FahLogger;
      };
    };
    /* eslint-disable @typescript-eslint/unbound-method */
    instance.sysap.logger.debug("Debug");
    expect(logger.debug).toHaveBeenCalledWith("Debug");
    instance.sysap.logger.error("Error");
    expect(logger.error).toHaveBeenCalledWith("Error");
    instance.sysap.logger.log("Log");
    expect(logger.info).toHaveBeenCalledWith("Log");
    instance.sysap.logger.warn("Warn");
    expect(logger.warn).toHaveBeenCalledWith("Warn");
    /* eslint-enable @typescript-eslint/unbound-method */
  });

  it("should configure free@home accessory", () => {
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    const instance = platform as unknown as {
      readonly sysap: {
        readonly logger: FahLogger;
      };
    };
    expect(platform.accessories.length).toBe(0);
    const platformAccessory = createPlatformAccessory("Test Accessory");
    platformAccessory.context = {
      channel: {},
      channelId: "ch1234",
      device: {},
      deviceSerial: "ABB7xxxxxxxx",
    };
    if (!isFreeAtHomeAccessory(platformAccessory, instance.sysap.logger)) {
      fail();
      return;
    }
    platform.configureAccessory(platformAccessory);
    expect(platform.accessories.length).toBe(1);
    expect(platform.accessories[0]).toBe(platformAccessory);
  });

  it("should not configure non-free@home accessory", () => {
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    expect(platform.accessories.length).toBe(0);
    const platformAccessory = createPlatformAccessory("Test Accessory");
    platformAccessory.context = {};
    platform.configureAccessory(platformAccessory);
    expect(platform.accessories.length).toBe(0);
  });

  it("should configure only allowed accessory", () => {
    config.allowedChannels = ["ABB700000001/*", "ABB700000002/ch0000"];
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    expect(platform.accessories.length).toBe(0);
    const platformAccessory = createPlatformAccessory("Test Accessory");
    platformAccessory.context = {
      channel: {},
      channelId: "ch0005",
      device: {},
      deviceSerial: "ABB700000002",
    };
    platform.configureAccessory(platformAccessory);
    expect(platform.accessories.length).toBe(0);
  });

  it("should not configure ignored channel from allowed device", () => {
    config.allowedChannels = ["ABB700000001/*"];
    config.ignoredChannels = ["ABB700000001/ch0005"];
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    expect(platform.accessories.length).toBe(0);
    const platformAccessory = createPlatformAccessory("Test Accessory");
    platformAccessory.context = {
      channel: {},
      channelId: "ch0005",
      device: {},
      deviceSerial: "ABB700000002",
    };
    platform.configureAccessory(platformAccessory);
    expect(platform.accessories.length).toBe(0);
  });

  it("should not configure ignored accessory", () => {
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    expect(platform.accessories.length).toBe(0);
    const platformAccessory = createPlatformAccessory("Test Accessory");
    platformAccessory.context = {
      channel: {},
      channelId: "ch0005",
      device: {},
      deviceSerial: "ABB700000002",
    };
    platform.configureAccessory(platformAccessory);
    expect(platform.accessories.length).toBe(0);
  });

  it("should discover devices from the system access point", async () => {
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    const instance = platform as unknown as {
      readonly fahAccessories: Map<string, FreeAtHomeAccessory>;
      readonly sysap: {
        readonly logger: FahLogger;
      };
      discoverDevices(): Promise<void>;
    };
    const knownAccessory = createPlatformAccessory(
      "Known Device",
      api.hap.uuid.generate("ABB700000002_ch0000")
    );
    knownAccessory.context = {
      channel: {
        functionID: "23",
      },
      channelId: "ch0000",
      device: {
        floor: "1",
        room: "1",
        channels: {
          ch0000: {
            functionID: "23",
          },
        },
      },
      deviceSerial: "ABB700000002",
    };
    if (!isFreeAtHomeAccessory(knownAccessory, instance.sysap.logger)) {
      fail();
      return;
    }

    platform.accessories.push(knownAccessory);
    spyOn(platform.sysap, "getConfiguration").and.resolveTo(configuration);
    await instance.discoverDevices();
    expect(instance.fahAccessories.size).toBe(15);
  });

  it("should discover devices from the system access point with allow list", async () => {
    config.allowedChannels = ["ABB700000001/*", "ABB700000002/ch0000"];

    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    const instance = platform as unknown as {
      readonly fahAccessories: Map<string, FreeAtHomeAccessory>;
      readonly sysap: {
        readonly logger: FahLogger;
      };
      discoverDevices(): Promise<void>;
    };
    spyOn(platform.sysap, "getConfiguration").and.resolveTo(configuration);
    await instance.discoverDevices();
    expect(instance.fahAccessories.size).toBe(3);
  });

  it("should discover devices from the system access point without ignore list", async () => {
    delete config.ignoredChannels;

    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    const instance = platform as unknown as {
      readonly fahAccessories: Map<string, FreeAtHomeAccessory>;
      readonly sysap: {
        readonly logger: FahLogger;
      };
      discoverDevices(): Promise<void>;
    };
    spyOn(platform.sysap, "getConfiguration").and.resolveTo(configuration);
    await instance.discoverDevices();
    expect(instance.fahAccessories.size).toBe(18);
  });

  it("should discover devices from the system access point in experimental mode", async () => {
    config.experimental = true;
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    // eslint-disable-next-line @typescript-eslint/unbound-method
    expect(logger.warn).toHaveBeenCalledWith("Experimental Mode enabled!");
    const instance = platform as unknown as {
      readonly fahAccessories: Map<string, FreeAtHomeAccessory>;
      readonly sysap: {
        readonly logger: FahLogger;
      };
      discoverDevices(): Promise<void>;
    };
    spyOn(platform.sysap, "getConfiguration").and.resolveTo(configuration);
    await instance.discoverDevices();
    expect(instance.fahAccessories.size).toBe(20);
  });

  it("should log an error if channel configuration fails during discovery", async () => {
    config.experimental = true;
    const testConfig: Configuration = {
      "00000000-0000-0000-0000-000000000000": {
        sysapName: "Gerke",
        devices: {
          ABB700000001: {
            channels: {
              ch0000: {
                displayName: "Scene Sensor",
                functionID: "6",
                floor: "1",
                room: "1",
              },
            },
          },
        },
        floorplan: {
          floors: {},
        },
        users: {},
      },
    };
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    const instance = platform as unknown as {
      readonly fahAccessories: Map<string, FreeAtHomeAccessory>;
      readonly sysap: {
        readonly logger: FahLogger;
      };
      discoverDevices(): Promise<void>;
    };
    spyOn(platform.sysap, "getConfiguration").and.resolveTo(testConfig);
    await instance.discoverDevices();
    expect(instance.fahAccessories.size).toBe(0);
    // eslint-disable-next-line @typescript-eslint/unbound-method
    expect(logger.error).toHaveBeenCalledWith(
      "Error processing discovered channel ABB700000001/ch0000",
      new Error("Data point object is undefined")
    );
  });

  it("should not create an accessory for an unknown function ID", () => {
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    const instance = platform as unknown as {
      readonly fahAccessories: Map<string, FreeAtHomeAccessory>;
      readonly sysap: {
        readonly logger: FahLogger;
      };
      createAccessory: (
        serial: string,
        functionID: string,
        channelId: string,
        accessory: PlatformAccessory<FreeAtHomeContext>
      ) => void;
    };
    const platformAccessory = createPlatformAccessory(
      "Known Device",
      api.hap.uuid.generate("ABB700000002_ch0000")
    );
    platformAccessory.context = {
      channel: {
        functionID: "23",
      },
      channelId: "ch0000",
      device: {
        floor: "1",
        room: "1",
        channels: {
          ch0000: {
            functionID: "23",
          },
        },
      },
      deviceSerial: "ABB700000002",
    };
    if (!isFreeAtHomeAccessory(platformAccessory, instance.sysap.logger)) {
      fail();
      return;
    }

    const spy = spyOn(instance.fahAccessories, "set");
    instance.createAccessory("", "never", "", platformAccessory);
    expect(spy).not.toHaveBeenCalled();
  });

  it("should process web socket message", () => {
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    const instance = platform as unknown as {
      readonly fahAccessories: Map<string, FreeAtHomeAccessory>;
      readonly sysap: {
        readonly logger: FahLogger;
      };
      processWebSocketMesage: (message: WebSocketMessage) => void;
    };
    const platformAccessory = createPlatformAccessory(
      "Known Device",
      api.hap.uuid.generate("ABB700000002_ch0000")
    );
    platformAccessory.context = {
      channel: {
        functionID: "23",
      },
      channelId: "ch0000",
      device: {
        floor: "1",
        room: "1",
        channels: {
          ch0000: {
            functionID: "23",
          },
        },
      },
      deviceSerial: "ABB700000002",
    };
    if (!isFreeAtHomeAccessory(platformAccessory, instance.sysap.logger)) {
      fail();
      return;
    }

    const accessory = new TestAccessory(platform, platformAccessory);
    const spy = spyOn(accessory, "updateDatapoint");
    instance.fahAccessories.set("ABB700000002_ch0000", accessory);
    const message: WebSocketMessage = {
      "00000000-0000-0000-0000-000000000000": {
        datapoints: {
          "ABB700000002/ch0000/idp0000": "1",
          "ABB700000002/ch0000/odp0000": "2",
          "ABB700000000/ch0000/odp0000": "3",
          "ABB700000000/ch0000/xdp0000": "0",
        },
        devices: {},
        devicesAdded: [],
        devicesRemoved: [],
        scenesTriggered: {},
      },
    };
    instance.processWebSocketMesage(message);
    expect(spy.calls.count()).toBe(2);
    expect(spy).toHaveBeenCalledWith("idp0000", "1");
    expect(spy).toHaveBeenCalledWith("odp0000", "2");
  });

  it("should resolve to mapped type", async () => {
    config.typeMappings = [{ channel: "ABB700000005/ch0001", type: "Outlet" }];
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    const instance = platform as unknown as {
      readonly fahAccessories: Map<string, FreeAtHomeAccessory>;
      discoverDevices(): Promise<void>;
    };
    spyOn(platform.sysap, "getConfiguration").and.resolveTo(configuration);
    await instance.discoverDevices();
    const accessory = instance.fahAccessories.get("ABB700000005_ch0001");
    expect(accessory).toBeDefined();
    expect(accessory).toBeInstanceOf(SwitchActuatorAccessory);
    expect((accessory as SwitchActuatorAccessory).accessoryType).toBe(
      AccessoryType.Outlet
    );
  });

  it("should resolve to unknown mapped types to Undefined", async () => {
    config.typeMappings = [{ channel: "ABB700000005/ch0001", type: "outlet" }];
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    const instance = platform as unknown as {
      readonly fahAccessories: Map<string, FreeAtHomeAccessory>;
      discoverDevices(): Promise<void>;
    };
    spyOn(platform.sysap, "getConfiguration").and.resolveTo(configuration);
    await instance.discoverDevices();
    const accessory = instance.fahAccessories.get("ABB700000005_ch0001");
    expect(accessory).toBeDefined();
    expect(accessory).toBeInstanceOf(SwitchActuatorAccessory);
    expect((accessory as SwitchActuatorAccessory).accessoryType).toBe(
      AccessoryType.Undefined
    );
  });

  it("should use the first mapping rule to resolve the mapped types if a channel has multiple mappings", async () => {
    config.typeMappings = [
      { channel: "ABB700000005/ch0001", type: "Outlet" },
      { channel: "ABB700000005/ch0001", type: "outlet" },
    ];
    const platform = new FreeAtHomeHomebridgePlatform(logger, config, api);
    const instance = platform as unknown as {
      readonly fahAccessories: Map<string, FreeAtHomeAccessory>;
      discoverDevices(): Promise<void>;
    };
    spyOn(platform.sysap, "getConfiguration").and.resolveTo(configuration);
    await instance.discoverDevices();
    const accessory = instance.fahAccessories.get("ABB700000005_ch0001");
    // eslint-disable-next-line @typescript-eslint/unbound-method
    expect(logger.warn).toHaveBeenCalledWith(
      "Multiple type mappings are defined for channel 'ABB700000005/CH0001'. The first mapping is used."
    );
    expect(accessory).toBeDefined();
    expect(accessory).toBeInstanceOf(SwitchActuatorAccessory);
    expect((accessory as SwitchActuatorAccessory).accessoryType).toBe(
      AccessoryType.Outlet
    );
  });
});
