/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.

SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/

import { type MockedObject } from "jest-mock";
import {
    type MatrixClient,
    MatrixEvent,
    type Beacon,
    getBeaconInfoIdentifier,
    ContentHelpers,
    type LocationAssetType,
    M_BEACON,
    M_BEACON_INFO,
} from "matrix-js-sdk/src/matrix";

import { getMockGeolocationPositionError } from "./location";
import { makeRoomWithStateEvents } from "./room";

type InfoContentProps = {
    timeout: number;
    isLive?: boolean;
    assetType?: LocationAssetType;
    description?: string;
    timestamp?: number;
};
const DEFAULT_INFO_CONTENT_PROPS: InfoContentProps = {
    timeout: 3600000,
};

/**
 * Create an m.beacon_info event
 * all required properties are mocked
 * override with contentProps
 */
export const makeBeaconInfoEvent = (
    sender: string,
    roomId: string,
    contentProps: Partial<InfoContentProps> = {},
    eventId?: string,
): MatrixEvent => {
    const { timeout, isLive, description, assetType, timestamp } = {
        ...DEFAULT_INFO_CONTENT_PROPS,
        ...contentProps,
    };
    const event = new MatrixEvent({
        type: M_BEACON_INFO.name,
        room_id: roomId,
        state_key: sender,
        sender,
        content: ContentHelpers.makeBeaconInfoContent(timeout, isLive, description, assetType, timestamp),
    });

    event.event.origin_server_ts = Date.now();

    // live beacons use the beacon_info event id
    // set or default this
    event.replaceLocalEventId(eventId || `$${Math.random()}-${Math.random()}`);

    return event;
};

type ContentProps = {
    geoUri: string;
    timestamp: number;
    beaconInfoId: string;
    description?: string;
};
const DEFAULT_CONTENT_PROPS: ContentProps = {
    geoUri: "geo:-36.24484561954707,175.46884959563613;u=10",
    timestamp: 123,
    beaconInfoId: "$123",
};

/**
 * Create an m.beacon event
 * all required properties are mocked
 * override with contentProps
 */
export const makeBeaconEvent = (
    sender: string,
    contentProps: Partial<ContentProps> = {},
    roomId?: string,
): MatrixEvent => {
    const { geoUri, timestamp, beaconInfoId, description } = {
        ...DEFAULT_CONTENT_PROPS,
        ...contentProps,
    };

    return new MatrixEvent({
        type: M_BEACON.name,
        room_id: roomId,
        sender,
        content: ContentHelpers.makeBeaconContent(geoUri, timestamp, beaconInfoId, description),
        origin_server_ts: 0,
    });
};

/**
 * Create a mock geolocation position
 * defaults all required properties
 */
export const makeGeolocationPosition = ({
    timestamp,
    coords,
}: {
    timestamp?: number;
    coords?: Partial<GeolocationCoordinates>;
}): GeolocationPosition =>
    ({
        timestamp: timestamp ?? 1647256791840,
        coords: {
            accuracy: 1,
            latitude: 54.001927,
            longitude: -8.253491,
            altitude: null,
            altitudeAccuracy: null,
            heading: null,
            speed: null,
            ...coords,
        },
    }) as unknown as GeolocationPosition;

/**
 * Creates a basic mock of Geolocation
 * sets navigator.geolocation to the mock
 * and returns mock
 */
export const mockGeolocation = (): MockedObject<Geolocation> => {
    const mockGeolocation = {
        clearWatch: jest.fn(),
        getCurrentPosition: jest.fn().mockImplementation((callback) => callback(makeGeolocationPosition({}))),
        watchPosition: jest.fn().mockImplementation((callback) => callback(makeGeolocationPosition({}))),
    } as unknown as MockedObject<Geolocation>;

    // jest jsdom does not provide geolocation
    // @ts-ignore illegal assignment to readonly property
    navigator.geolocation = mockGeolocation;

    return mockGeolocation;
};

/**
 * Creates a mock watchPosition implementation
 * that calls success callback at the provided delays
 * ```
 * geolocation.watchPosition.mockImplementation([0, 1000, 5000, 50])
 * ```
 * will call the provided handler with a mock position at
 * next tick, 1000ms, 6000ms, 6050ms
 *
 * to produce errors provide an array of error codes
 * that will be applied to the delay with the same index
 * eg:
 * ```
 * // return two good positions, then a permission denied error
 * geolocation.watchPosition.mockImplementation(watchPositionMockImplementation(
 *      [0, 1000, 3000], [0, 0, 1]),
 * );
 * ```
 * See for error codes: https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPositionError
 */
export const watchPositionMockImplementation = (delays: number[], errorCodes: number[] = []) => {
    return (callback: PositionCallback, error: PositionErrorCallback): number => {
        const position = makeGeolocationPosition({});

        let totalDelay = 0;
        delays.map((delayMs, index) => {
            totalDelay += delayMs;
            const timeout = window.setTimeout(() => {
                if (errorCodes[index]) {
                    error(getMockGeolocationPositionError(errorCodes[index], "error message"));
                } else {
                    callback({ ...position, timestamp: position.timestamp + totalDelay });
                }
            }, totalDelay);
            return timeout;
        });

        return totalDelay;
    };
};

/**
 * Creates a room with beacon events
 * sets given locations on beacons
 * returns beacons
 */
export const makeRoomWithBeacons = (
    roomId: string,
    mockClient: MockedObject<MatrixClient>,
    beaconInfoEvents: MatrixEvent[],
    locationEvents?: MatrixEvent[],
): Beacon[] => {
    const room = makeRoomWithStateEvents(beaconInfoEvents, { roomId, mockClient });
    const beacons = beaconInfoEvents.map((event) => room.currentState.beacons.get(getBeaconInfoIdentifier(event))!);
    if (locationEvents) {
        beacons.forEach((beacon) => {
            // this filtering happens in roomState, which is bypassed here
            const validLocationEvents = locationEvents?.filter((event) => event.getSender() === beacon.beaconInfoOwner);
            beacon.addLocations(validLocationEvents);
        });
    }
    return beacons;
};
