/*
 *   Ansible inventory script used at Famedly GmbH for managing many hosts
 *   Copyright (C) 2019, 2020 Famedly GmbH
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Affero General Public License as
 *   published by the Free Software Foundation, either version 3 of the
 *   License, or (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *   GNU Affero General Public License for more details.
 *
 *   You should have received a copy of the GNU Affero General Public License
 *   along with this program.  If not, see .
 */
import 'dart:convert';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:test/test.dart';
import 'fake_matrix_api.dart';
import 'fake_database.dart';
Map jsonDecode(dynamic payload) {
  if (payload is String) {
    try {
      return json.decode(payload);
    } catch (e) {
      return {};
    }
  }
  if (payload is Map) return payload;
  return {};
}
void main() {
  /// All Tests related to device keys
  test('fromJson', () async {
    var rawJson = {
      'content': {
        'action': 'request',
        'body': {
          'algorithm': 'm.megolm.v1.aes-sha2',
          'room_id': '!726s6s6q:example.com',
          'sender_key': 'RF3s+E7RkTQTGF2d8Deol0FkQvgII2aJDf3/Jp5mxVU',
          'session_id': 'X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ'
        },
        'request_id': '1495474790150.19',
        'requesting_device_id': 'JLAFKJWSCS'
      },
      'type': 'm.room_key_request',
      'sender': '@alice:example.com'
    };
    var toDeviceEvent = ToDeviceEvent.fromJson(rawJson);
    expect(toDeviceEvent.content, rawJson['content']);
    expect(toDeviceEvent.sender, rawJson['sender']);
    expect(toDeviceEvent.type, rawJson['type']);
    expect(
      ToDeviceEventDecryptionError(
              exception: Exception('test'),
              stackTrace: null,
              toDeviceEvent: toDeviceEvent)
          .sender,
      rawJson['sender'],
    );
    var matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi());
    matrix.database = getDatabase();
    await matrix.checkServer('https://fakeServer.notExisting');
    await matrix.login('test', '1234');
    var room = matrix.getRoomById('!726s6s6q:example.com');
    if (matrix.encryptionEnabled) {
      await room.createOutboundGroupSession();
      rawJson['content']['body']['session_id'] =
          room.inboundGroupSessions.keys.first;
      var roomKeyRequest = RoomKeyRequest.fromToDeviceEvent(
          ToDeviceEvent.fromJson(rawJson),
          matrix.keyManager,
          KeyManagerKeyShareRequest(
            room: room,
            sessionId: rawJson['content']['body']['session_id'],
            senderKey: rawJson['content']['body']['sender_key'],
            devices: [
              matrix.userDeviceKeys[rawJson['sender']]
                  .deviceKeys[rawJson['content']['requesting_device_id']]
            ],
          ));
      await roomKeyRequest.forwardKey();
    }
    await matrix.dispose(closeDatabase: true);
  });
  test('Create Request', () async {
    var matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi());
    matrix.database = getDatabase();
    await matrix.checkServer('https://fakeServer.notExisting');
    await matrix.login('test', '1234');
    if (!matrix.encryptionEnabled) {
      await matrix.dispose(closeDatabase: true);
      return;
    }
    final requestRoom = matrix.getRoomById('!726s6s6q:example.com');
    await matrix.keyManager.request(requestRoom, 'sessionId', 'senderKey');
    var foundEvent = false;
    for (var entry in FakeMatrixApi.calledEndpoints.entries) {
      final payload = jsonDecode(entry.value.first);
      if (entry.key.startsWith('/client/r0/sendToDevice/m.room_key_request') &&
          (payload['messages'] is Map) &&
          (payload['messages']['@alice:example.com'] is Map) &&
          (payload['messages']['@alice:example.com']['*'] is Map)) {
        final content = payload['messages']['@alice:example.com']['*'];
        if (content['action'] == 'request' &&
            content['body']['room_id'] == '!726s6s6q:example.com' &&
            content['body']['sender_key'] == 'senderKey' &&
            content['body']['session_id'] == 'sessionId') {
          foundEvent = true;
          break;
        }
      }
    }
    expect(foundEvent, true);
    await matrix.dispose(closeDatabase: true);
  });
  final validSessionId = 'ciM/JWTPrmiWPPZNkRLDPQYf9AW/I46bxyLSr+Bx5oU';
  test('Reply To Request', () async {
    var matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi());
    matrix.database = getDatabase();
    await matrix.checkServer('https://fakeServer.notExisting');
    await matrix.login('test', '1234');
    if (!matrix.encryptionEnabled) {
      await matrix.dispose(closeDatabase: true);
      return;
    }
    matrix.setUserId('@alice:example.com'); // we need to pretend to be alice
    FakeMatrixApi.calledEndpoints.clear();
    await matrix.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE']
        .setBlocked(false, matrix);
    await matrix.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE']
        .setVerified(true, matrix);
    await matrix.userDeviceKeys['@alice:example.com'].deviceKeys['OTHERDEVICE']
        .startVerification(matrix);
    // test a successful share
    var event = ToDeviceEvent(
        sender: '@alice:example.com',
        type: 'm.room_key_request',
        content: {
          'action': 'request',
          'body': {
            'algorithm': 'm.megolm.v1.aes-sha2',
            'room_id': '!726s6s6q:example.com',
            'sender_key': 'senderKey',
            'session_id': validSessionId,
          },
          'request_id': 'request_1',
          'requesting_device_id': 'OTHERDEVICE',
        });
    await matrix.keyManager.handleToDeviceEvent(event);
    expect(
        FakeMatrixApi.calledEndpoints.keys.any(
            (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
        true);
    // test various fail scenarios
    // no body
    FakeMatrixApi.calledEndpoints.clear();
    event = ToDeviceEvent(
        sender: '@alice:example.com',
        type: 'm.room_key_request',
        content: {
          'action': 'request',
          'request_id': 'request_2',
          'requesting_device_id': 'OTHERDEVICE',
        });
    await matrix.keyManager.handleToDeviceEvent(event);
    expect(
        FakeMatrixApi.calledEndpoints.keys.any(
            (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
        false);
    // request by ourself
    FakeMatrixApi.calledEndpoints.clear();
    event = ToDeviceEvent(
        sender: '@alice:example.com',
        type: 'm.room_key_request',
        content: {
          'action': 'request',
          'body': {
            'algorithm': 'm.megolm.v1.aes-sha2',
            'room_id': '!726s6s6q:example.com',
            'sender_key': 'senderKey',
            'session_id': validSessionId,
          },
          'request_id': 'request_3',
          'requesting_device_id': 'JLAFKJWSCS',
        });
    await matrix.keyManager.handleToDeviceEvent(event);
    expect(
        FakeMatrixApi.calledEndpoints.keys.any(
            (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
        false);
    // device not found
    FakeMatrixApi.calledEndpoints.clear();
    event = ToDeviceEvent(
        sender: '@alice:example.com',
        type: 'm.room_key_request',
        content: {
          'action': 'request',
          'body': {
            'algorithm': 'm.megolm.v1.aes-sha2',
            'room_id': '!726s6s6q:example.com',
            'sender_key': 'senderKey',
            'session_id': validSessionId,
          },
          'request_id': 'request_4',
          'requesting_device_id': 'blubb',
        });
    await matrix.keyManager.handleToDeviceEvent(event);
    expect(
        FakeMatrixApi.calledEndpoints.keys.any(
            (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
        false);
    // unknown room
    FakeMatrixApi.calledEndpoints.clear();
    event = ToDeviceEvent(
        sender: '@alice:example.com',
        type: 'm.room_key_request',
        content: {
          'action': 'request',
          'body': {
            'algorithm': 'm.megolm.v1.aes-sha2',
            'room_id': '!invalid:example.com',
            'sender_key': 'senderKey',
            'session_id': validSessionId,
          },
          'request_id': 'request_5',
          'requesting_device_id': 'OTHERDEVICE',
        });
    await matrix.keyManager.handleToDeviceEvent(event);
    expect(
        FakeMatrixApi.calledEndpoints.keys.any(
            (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
        false);
    // unknwon session
    FakeMatrixApi.calledEndpoints.clear();
    event = ToDeviceEvent(
        sender: '@alice:example.com',
        type: 'm.room_key_request',
        content: {
          'action': 'request',
          'body': {
            'algorithm': 'm.megolm.v1.aes-sha2',
            'room_id': '!726s6s6q:example.com',
            'sender_key': 'senderKey',
            'session_id': 'invalid',
          },
          'request_id': 'request_6',
          'requesting_device_id': 'OTHERDEVICE',
        });
    await matrix.keyManager.handleToDeviceEvent(event);
    expect(
        FakeMatrixApi.calledEndpoints.keys.any(
            (k) => k.startsWith('/client/r0/sendToDevice/m.room.encrypted')),
        false);
    FakeMatrixApi.calledEndpoints.clear();
    await matrix.dispose(closeDatabase: true);
  });
  test('Receive shared keys', () async {
    var matrix = Client('testclient', debug: true, httpClient: FakeMatrixApi());
    matrix.database = getDatabase();
    await matrix.checkServer('https://fakeServer.notExisting');
    await matrix.login('test', '1234');
    if (!matrix.encryptionEnabled) {
      await matrix.dispose(closeDatabase: true);
      return;
    }
    final requestRoom = matrix.getRoomById('!726s6s6q:example.com');
    await matrix.keyManager.request(requestRoom, validSessionId, 'senderKey');
    final session = requestRoom.inboundGroupSessions[validSessionId];
    final sessionKey = session.inboundGroupSession
        .export_session(session.inboundGroupSession.first_known_index());
    requestRoom.inboundGroupSessions.clear();
    var event = ToDeviceEvent(
        sender: '@alice:example.com',
        type: 'm.forwarded_room_key',
        content: {
          'algorithm': 'm.megolm.v1.aes-sha2',
          'room_id': '!726s6s6q:example.com',
          'session_id': validSessionId,
          'session_key': sessionKey,
          'sender_key': 'senderKey',
          'forwarding_curve25519_key_chain': [],
        },
        encryptedContent: {
          'sender_key': '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI',
        });
    await matrix.keyManager.handleToDeviceEvent(event);
    expect(requestRoom.inboundGroupSessions.containsKey(validSessionId), true);
    // now test a few invalid scenarios
    // request not found
    requestRoom.inboundGroupSessions.clear();
    event = ToDeviceEvent(
        sender: '@alice:example.com',
        type: 'm.forwarded_room_key',
        content: {
          'algorithm': 'm.megolm.v1.aes-sha2',
          'room_id': '!726s6s6q:example.com',
          'session_id': validSessionId,
          'session_key': sessionKey,
          'sender_key': 'senderKey',
          'forwarding_curve25519_key_chain': [],
        },
        encryptedContent: {
          'sender_key': '3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI',
        });
    await matrix.keyManager.handleToDeviceEvent(event);
    expect(requestRoom.inboundGroupSessions.containsKey(validSessionId), false);
    // unknown device
    await matrix.keyManager.request(requestRoom, validSessionId, 'senderKey');
    requestRoom.inboundGroupSessions.clear();
    event = ToDeviceEvent(
        sender: '@alice:example.com',
        type: 'm.forwarded_room_key',
        content: {
          'algorithm': 'm.megolm.v1.aes-sha2',
          'room_id': '!726s6s6q:example.com',
          'session_id': validSessionId,
          'session_key': sessionKey,
          'sender_key': 'senderKey',
          'forwarding_curve25519_key_chain': [],
        },
        encryptedContent: {
          'sender_key': 'invalid',
        });
    await matrix.keyManager.handleToDeviceEvent(event);
    expect(requestRoom.inboundGroupSessions.containsKey(validSessionId), false);
    // no encrypted content
    await matrix.keyManager.request(requestRoom, validSessionId, 'senderKey');
    requestRoom.inboundGroupSessions.clear();
    event = ToDeviceEvent(
        sender: '@alice:example.com',
        type: 'm.forwarded_room_key',
        content: {
          'algorithm': 'm.megolm.v1.aes-sha2',
          'room_id': '!726s6s6q:example.com',
          'session_id': validSessionId,
          'session_key': sessionKey,
          'sender_key': 'senderKey',
          'forwarding_curve25519_key_chain': [],
        });
    await matrix.keyManager.handleToDeviceEvent(event);
    expect(requestRoom.inboundGroupSessions.containsKey(validSessionId), false);
    await matrix.dispose(closeDatabase: true);
  });
}