/*
 *   Famedly Matrix SDK
 *   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:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:canonical_json/canonical_json.dart';
import 'package:collection/collection.dart';
import 'package:olm/olm.dart' as olm;
import 'package:path/path.dart' show join;
import 'package:test/test.dart';
import 'package:matrix/matrix.dart';
import 'package:matrix/src/utils/client_init_exception.dart';
import 'fake_client.dart';
import 'fake_database.dart';
void main() {
  // key @test:fakeServer.notExisting
  const pickledOlmAccount =
      'N2v1MkIFGcl0mQpo2OCwSopxPQJ0wnl7oe7PKiT4141AijfdTIhRu+ceXzXKy3Kr00nLqXtRv7kid6hU4a+V0rfJWLL0Y51+3Rp/ORDVnQy+SSeo6Fn4FHcXrxifJEJ0djla5u98fBcJ8BSkhIDmtXRPi5/oJAvpiYn+8zMjFHobOeZUAxYR0VfQ9JzSYBsSovoQ7uFkNks1M4EDUvHtuyg3RxViwdNxs3718fyAqQ/VSwbXsY0Nl+qQbF+nlVGHenGqk5SuNl1P6e1PzZxcR0IfXA94Xij1Ob5gDv5YH4UCn9wRMG0abZsQP0YzpDM0FLaHSCyo9i5JD/vMlhH+nZWrgAzPPCTNGYewNV8/h3c+VyJh8ZTx/fVi6Yq46Fv+27Ga2ETRZ3Qn+Oyx6dLBjnBZ9iUvIhqpe2XqaGA1PopOz8iDnaZitw';
  const identityKey = '7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk';
  const fingerprintKey = 'gjL//fyaFHADt9KBADGag8g7F8Up78B/K1zXeiEPLJo';
  group('client path', () {
    late Client clientOnPath;
    final dbPath = join(Directory.current.path, 'test.sqlite');
    setUp(() async {
      expect(
        await File(dbPath).exists(),
        false,
        reason: '$dbPath should not exist',
      );
      clientOnPath = await getClient(
        databasePath: dbPath,
      );
      await clientOnPath.abortSync();
      expect(await File(dbPath).exists(), true, reason: '$dbPath should exist');
    });
    test('logout', () async {
      expect(await File(dbPath).exists(), true);
      await clientOnPath.logout();
      expect(await File(dbPath).exists(), false);
    });
  });
  /// All Tests related to the Login
  group('client mem', tags: 'olm', () {
    late Client matrix;
    Logs().level = Level.error;
    /// Check if all Elements get created
    setUp(() async {
      matrix = await getClient();
    });
    test('barebones client login', () async {
      final client = Client(
        'testclient',
        httpClient: FakeMatrixApi(),
        databaseBuilder: getDatabase,
      );
      expect(client.isLogged(), false);
      final Set initStates = {};
      await client.init(onInitStateChanged: initStates.add);
      expect(client.isLogged(), false);
      expect(initStates, {InitState.initializing, InitState.finished});
      initStates.clear();
      await client.login(
        LoginType.mLoginPassword,
        token: 'abcd',
        identifier:
            AuthenticationUserIdentifier(user: '@test:fakeServer.notExisting'),
        deviceId: 'GHTYAJCE',
        onInitStateChanged: initStates.add,
      );
      expect(initStates, {
        InitState.initializing,
        InitState.settingUpEncryption,
        InitState.loadingData,
        InitState.waitingForFirstSync,
        InitState.finished,
      });
      expect(client.isLogged(), true);
      await client.logout();
    });
    test('Login', () async {
      matrix = Client(
        'testclient',
        httpClient: FakeMatrixApi(),
        databaseBuilder: getDatabase,
      );
      final eventUpdateListFuture = matrix.onTimelineEvent.stream.toList();
      final toDeviceUpdateListFuture = matrix.onToDeviceEvent.stream.toList();
      var presenceCounter = 0;
      var accountDataCounter = 0;
      matrix.onPresenceChanged.stream.listen((CachedPresence data) {
        presenceCounter++;
      });
      // ignore: deprecated_member_use_from_same_package
      matrix.onAccountData.stream.listen((BasicEvent data) {
        accountDataCounter++;
      });
      expect(matrix.homeserver, null);
      try {
        await matrix
            .checkHomeserver(Uri.parse('https://fakeserver.wrongaddress'));
      } catch (exception) {
        expect(exception.toString().isNotEmpty, true);
      }
      await matrix.checkHomeserver(
        Uri.parse('https://fakeserver.notexisting'),
        checkWellKnown: false,
      );
      expect(matrix.homeserver.toString(), 'https://fakeserver.notexisting');
      final available = await matrix.checkUsernameAvailability('testuser');
      expect(available, true);
      final loginStateFuture = matrix.onLoginStateChanged.stream.first;
      final syncFuture = matrix.onSync.stream.first;
      await matrix.init(
        newToken: 'abcd',
        newUserID: '@test:fakeServer.notExisting',
        newHomeserver: matrix.homeserver,
        newDeviceName: 'Text Matrix Client',
        newDeviceID: 'GHTYAJCE',
        newOlmAccount: pickledOlmAccount,
      );
      final loginState = await loginStateFuture;
      final sync = await syncFuture;
      // to ensure our state doesn't get overwritten once we manually inject SyncUpdates
      await matrix.abortSync();
      expect(loginState, LoginState.loggedIn);
      expect(matrix.onSync.value != null, true);
      expect(matrix.encryptionEnabled, true);
      expect(matrix.identityKey, identityKey);
      expect(matrix.fingerprintKey, fingerprintKey);
      expect(sync.nextBatch == matrix.prevBatch, true);
      expect(matrix.accountData.length, 10);
      expect(
        matrix.getDirectChatFromUserId('@bob:example.com'),
        '!726s6s6q:example.com',
      );
      expect(matrix.rooms[2].directChatMatrixID, '@bob:example.com');
      expect(matrix.directChats, matrix.accountData['m.direct']?.content);
      // ignore: deprecated_member_use_from_same_package
      expect(matrix.presences.length, 1);
      expect(matrix.rooms[2].ephemerals.length, 2);
      expect(matrix.rooms[2].typingUsers.length, 1);
      expect(matrix.rooms[2].typingUsers[0].id, '@alice:example.com');
      expect(matrix.rooms[2].roomAccountData.length, 3);
      expect(matrix.rooms[2].encrypted, true);
      expect(
        matrix.rooms[2].encryptionAlgorithm,
        Client.supportedGroupEncryptionAlgorithms.first,
      );
      expect(
        matrix
            .rooms[2].receiptState.global.otherUsers['@alice:example.com']?.ts,
        1436451550453,
      );
      expect(
        matrix.rooms[2].receiptState.global.otherUsers['@alice:example.com']
            ?.eventId,
        '\$7365636s6r6432:example.com',
      );
      final inviteRoom = matrix.rooms
          .singleWhere((room) => room.membership == Membership.invite);
      expect(inviteRoom.name, 'My Room Name');
      expect(inviteRoom.states[EventTypes.RoomMember]?.length, 1);
      expect(matrix.rooms.length, 3);
      expect(
        matrix.rooms[2].canonicalAlias,
        "#famedlyContactDiscovery:${matrix.userID!.split(":")[1]}",
      );
      expect(
        // ignore: deprecated_member_use_from_same_package
        matrix.presences['@alice:example.com']?.presence,
        PresenceType.online,
      );
      expect(presenceCounter, 1);
      expect(accountDataCounter, 10);
      expect(matrix.userDeviceKeys.keys.toSet(), {
        '@alice:example.com',
        '@othertest:fakeServer.notExisting',
        '@test:fakeServer.notExisting',
      });
      expect(matrix.userDeviceKeys['@alice:example.com']?.outdated, false);
      expect(matrix.userDeviceKeys['@alice:example.com']?.deviceKeys.length, 2);
      expect(
        matrix.userDeviceKeys['@alice:example.com']?.deviceKeys['JLAFKJWSCS']
            ?.verified,
        false,
      );
      expect(matrix.wellKnown, isNull);
      await matrix.handleSync(
        SyncUpdate.fromJson({
          'next_batch': 'fakesync',
          'device_lists': {
            'changed': [
              '@alice:example.com',
            ],
            'left': [
              '@bob:example.com',
            ],
          },
        }),
      );
      await Future.delayed(Duration(milliseconds: 50));
      expect(matrix.userDeviceKeys.length, 3);
      expect(matrix.userDeviceKeys['@alice:example.com']?.outdated, true);
      await matrix.handleSync(
        SyncUpdate.fromJson({
          'next_batch': 'fakesync',
          'rooms': {
            'join': {
              '!726s6s6q:example.com': {
                'state': {
                  'events': [
                    {
                      'sender': '@alice:example.com',
                      'type': 'm.room.canonical_alias',
                      'content': {'alias': ''},
                      'state_key': '',
                      'origin_server_ts': 1417731086799,
                      'event_id': '66697273743033:example.com',
                    }
                  ],
                },
              },
            },
          },
        }),
      );
      await Future.delayed(Duration(milliseconds: 50));
      expect(
        matrix.getRoomByAlias(
          "#famedlyContactDiscovery:${matrix.userID!.split(":")[1]}",
        ),
        null,
      );
      await matrix.onTimelineEvent.close();
      final eventUpdateList = await eventUpdateListFuture;
      expect(eventUpdateList.length, 3);
      expect(eventUpdateList[0].type, 'm.room.member');
      expect(eventUpdateList[0].roomId, '!726s6s6q:example.com');
      expect(eventUpdateList[1].type, 'm.room.message');
      expect(eventUpdateList[1].roomId, '!726s6s6q:example.com');
      expect(eventUpdateList[2].type, 'm.room.message');
      expect(eventUpdateList[2].roomId, '!726s6s6f:example.com');
      expect(
        matrix
            .getRoomById('!726s6s6q:example.com')
            ?.roomAccountData['org.example.custom.room.config']
            ?.type,
        'org.example.custom.room.config',
      );
      expect(
        matrix
            .getRoomById('!726s6s6q:example.com')
            ?.roomAccountData[LatestReceiptState.eventType]
            ?.type,
        LatestReceiptState.eventType,
      );
      expect(
        matrix
            .getRoomById('!726s6s6q:example.com')
            ?.roomAccountData['m.tag']
            ?.type,
        'm.tag',
      );
      expect(
        matrix
            .getRoomById('!726s6s6q:example.com')
            ?.ephemerals['m.typing']
            ?.content,
        {
          'user_ids': ['@alice:example.com'],
        },
      );
      expect(
        matrix
            .getRoomById('!726s6s6q:example.com')
            ?.ephemerals['m.receipt']
            ?.content,
        {
          '\$7365636s6r6432:example.com': {
            'm.read': {
              '@alice:example.com': {'ts': 1436451550453},
            },
          },
        },
      );
      await matrix.onToDeviceEvent.close();
      final deviceeventUpdateList = await toDeviceUpdateListFuture;
      expect(deviceeventUpdateList.length, 2);
      expect(deviceeventUpdateList[0].type, 'm.new_device');
      expect(deviceeventUpdateList[1].type, 'm.room_key');
    });
    test('recentEmoji', () async {
      final emojis = matrix.recentEmojis;
      expect(emojis.length, 2);
      expect(emojis['👍️'], 1);
      expect(emojis['🖇️'], 0);
      await matrix.addRecentEmoji('🦙');
      // To check if the emoji is properly added, we need to wait for a sync roundtrip
    });
    test('accountData', () async {
      final content = {
        'bla': 'blub',
      };
      final key = 'abc def!/_-';
      await matrix.setAccountData(matrix.userID!, key, content);
      final dbContent = await matrix.database?.getAccountData();
      expect(matrix.accountData[key]?.content, content);
      expect(dbContent?[key]?.content, content);
    });
    test('roomAccountData', () async {
      final content = {
        'bla': 'blub',
      };
      final key = 'abc def!/_-';
      final roomId = '!726s6s6q:example.com';
      await matrix.setAccountDataPerRoom(matrix.userID!, roomId, key, content);
      final roomFromList = (await matrix.database?.getRoomList(matrix))
          ?.firstWhere((room) => room.id == roomId);
      final roomFromDb = await matrix.database?.getSingleRoom(matrix, roomId);
      expect(
        matrix.getRoomById(roomId)?.roomAccountData[key]?.content,
        content,
      );
      expect(roomFromList?.roomAccountData[key]?.content, content);
      expect(
        roomFromDb?.roomAccountData[key]?.content,
        content,
        skip: 'The single room function does not load account data',
      );
    });
    test('Logout', () async {
      final loginStateFuture = matrix.onLoginStateChanged.stream.first;
      await matrix.logout();
      expect(matrix.accessToken == null, true);
      expect(matrix.homeserver == null, true);
      expect(matrix.userID == null, true);
      expect(matrix.deviceID == null, true);
      expect(matrix.deviceName == null, true);
      expect(matrix.prevBatch == null, true);
      final loginState = await loginStateFuture;
      expect(loginState, LoginState.loggedOut);
    });
    test('Login again but break server when trying to logout', () async {
      matrix = Client(
        'testclient',
        httpClient: FakeMatrixApi(),
        databaseBuilder: getDatabase,
      );
      expect(matrix.homeserver, null);
      try {
        await matrix
            .checkHomeserver(Uri.parse('https://fakeserver.wrongaddress'));
      } catch (exception) {
        expect(exception.toString().isNotEmpty, true);
      }
      await matrix.checkHomeserver(
        Uri.parse('https://fakeserver.notexisting'),
        checkWellKnown: false,
      );
      expect(matrix.homeserver.toString(), 'https://fakeserver.notexisting');
      final available = await matrix.checkUsernameAvailability('testuser');
      expect(available, true);
      final loginStateFuture = matrix.onLoginStateChanged.stream.first;
      final syncFuture = matrix.onSync.stream.first;
      await matrix.init(
        newToken: 'abcd',
        newUserID: '@test:fakeServer.notExisting',
        newHomeserver: matrix.homeserver,
        newDeviceName: 'Text Matrix Client',
        newDeviceID: 'GHTYAJCE',
        newOlmAccount: pickledOlmAccount,
      );
      await Future.delayed(Duration(milliseconds: 50));
      final loginState = await loginStateFuture;
      final sync = await syncFuture;
      expect(loginState, LoginState.loggedIn);
      expect(matrix.onSync.value != null, true);
      expect(matrix.encryptionEnabled, true);
      expect(matrix.identityKey, identityKey);
      expect(matrix.fingerprintKey, fingerprintKey);
      expect(sync.nextBatch == matrix.prevBatch, true);
    });
    test('Logout but this time server is dead', () async {
      final oldapi = FakeMatrixApi.currentApi?.api;
      expect(
        (FakeMatrixApi.currentApi?.api['PUT']?.keys.where(
          (element) => element.startsWith('/client/v3/room_keys/keys?version'),
        ))?.length,
        1,
      );
      // not a huge fan, open to ideas
      FakeMatrixApi.currentApi?.api = {};
      // random sanity test to test if the test to test the breaking server hack works.
      expect(
        (FakeMatrixApi.currentApi?.api['PUT']?.keys.where(
          (element) => element.startsWith('/client/v3/room_keys/keys?version'),
        ))?.length,
        null,
      );
      try {
        await matrix.logout();
      } catch (e) {
        Logs().w(
          'Ignore red warnings for this test, test is to check if database is cleared even if server breaks ',
        );
      }
      expect(matrix.accessToken == null, true);
      expect(matrix.homeserver == null, true);
      expect(matrix.userID == null, true);
      expect(matrix.deviceID == null, true);
      expect(matrix.deviceName == null, true);
      expect(matrix.prevBatch == null, true);
      FakeMatrixApi.currentApi?.api = oldapi!;
    });
    test('Login', () async {
      matrix = Client(
        'testclient',
        httpClient: FakeMatrixApi(),
        databaseBuilder: getDatabase,
      );
      await matrix.checkHomeserver(
        Uri.parse('https://fakeserver.notexisting'),
        checkWellKnown: false,
      );
      final loginResp = await matrix.login(
        LoginType.mLoginPassword,
        identifier: AuthenticationUserIdentifier(user: 'test'),
        password: '1234',
      );
      expect(loginResp.userId.isNotEmpty, true);
    });
    test('setAvatar', () async {
      final testFile = MatrixFile(bytes: Uint8List(0), name: 'file.jpeg');
      await matrix.setAvatar(testFile);
    });
    test('setMuteAllPushNotifications', () async {
      await matrix.setMuteAllPushNotifications(false);
    });
    test('createSpace', () async {
      await matrix.createSpace(
        name: 'space',
        topic: 'My test space',
        spaceAliasName: '#myspace:example.invalid',
        invite: ['@alice:example.invalid'],
        roomVersion: '3',
      );
    });
    test('sync state event in-memory handling', () async {
      final roomId = '!726s6s6q:example.com';
      final room = matrix.getRoomById(roomId)!;
      // put an important state event in-memory
      await matrix.handleSync(
        SyncUpdate.fromJson({
          'next_batch': 'fakesync',
          'rooms': {
            'join': {
              roomId: {
                'state': {
                  'events': [
                    {
                      'sender': '@alice:example.com',
                      'type': 'm.room.name',
                      'content': {'name': 'foxies'},
                      'state_key': '',
                      'origin_server_ts': 1417731086799,
                      'event_id': '66697273743033:example.com',
                    }
                  ],
                },
              },
            },
          },
        }),
      );
      expect(room.getState('m.room.name')?.content['name'], 'foxies');
      // drop an unimportant state event from in-memory handling
      await matrix.handleSync(
        SyncUpdate.fromJson({
          'next_batch': 'fakesync',
          'rooms': {
            'join': {
              roomId: {
                'state': {
                  'events': [
                    {
                      'sender': '@alice:example.com',
                      'type': 'com.famedly.custom',
                      'content': {'name': 'foxies'},
                      'state_key': '',
                      'origin_server_ts': 1417731086799,
                      'event_id': '66697273743033:example.com',
                    }
                  ],
                },
              },
            },
          },
        }),
      );
      expect(room.getState('com.famedly.custom'), null);
      // persist normal room messages
      await matrix.handleSync(
        SyncUpdate.fromJson({
          'next_batch': 'fakesync',
          'rooms': {
            'join': {
              roomId: {
                'timeline': {
                  'events': [
                    {
                      'sender': '@alice:example.com',
                      'type': 'm.room.message',
                      'content': {
                        'msgtype': 'm.text',
                        'body': 'meow',
                      },
                      'origin_server_ts': 1417731086799,
                      'event_id': '\$last:example.com',
                    }
                  ],
                },
              },
            },
          },
        }),
      );
      expect(room.lastEvent!.content['body'], 'meow');
      // ignore edits
      await matrix.handleSync(
        SyncUpdate.fromJson({
          'next_batch': 'fakesync',
          'rooms': {
            'join': {
              roomId: {
                'timeline': {
                  'events': [
                    {
                      'sender': '@alice:example.com',
                      'type': 'm.room.message',
                      'content': {
                        'msgtype': 'm.text',
                        'body': '* floooof',
                        'm.new_content': {
                          'msgtype': 'm.text',
                          'body': 'floooof',
                        },
                        'm.relates_to': {
                          'rel_type': 'm.replace',
                          'event_id': '\$other:example.com',
                        },
                      },
                      'origin_server_ts': 1417731086799,
                      'event_id': '\$edit:example.com',
                    }
                  ],
                },
              },
            },
          },
        }),
      );
      expect(room.lastEvent!.content['body'], 'meow');
      // accept edits to the last event
      await matrix.handleSync(
        SyncUpdate.fromJson({
          'next_batch': 'fakesync',
          'rooms': {
            'join': {
              roomId: {
                'timeline': {
                  'events': [
                    {
                      'sender': '@alice:example.com',
                      'type': 'm.room.message',
                      'content': {
                        'msgtype': 'm.text',
                        'body': '* floooof',
                        'm.new_content': {
                          'msgtype': 'm.text',
                          'body': 'floooof',
                        },
                        'm.relates_to': {
                          'rel_type': 'm.replace',
                          'event_id': '\$last:example.com',
                        },
                      },
                      'origin_server_ts': 1417731086799,
                      'event_id': '\$edit:example.com',
                    }
                  ],
                },
              },
            },
          },
        }),
      );
      expect(room.lastEvent!.content['body'], '* floooof');
      // Older state event should not overwrite current state events
      room.partial = false;
      await matrix.handleSync(
        SyncUpdate(
          nextBatch: '',
          rooms: RoomsUpdate(
            join: {
              room.id: JoinedRoomUpdate(
                state: [
                  MatrixEvent(
                    type: EventTypes.RoomMember,
                    content: {'displayname': 'Alice Catgirl'},
                    senderId: '@alice:example.com',
                    eventId: 'oldEventId',
                    stateKey: '@alice:example.com',
                    originServerTs:
                        DateTime.now().subtract(const Duration(days: 365 * 30)),
                  ),
                ],
              ),
            },
          ),
        ),
        direction: Direction.b,
      );
      room.partial = true;
      expect(room.getParticipants().first.id, '@alice:example.com');
      expect(room.getParticipants().first.displayName, 'Alice Margatroid');
      // accepts a consecutive edit
      await matrix.handleSync(
        SyncUpdate.fromJson({
          'next_batch': 'fakesync',
          'rooms': {
            'join': {
              roomId: {
                'timeline': {
                  'events': [
                    {
                      'sender': '@alice:example.com',
                      'type': 'm.room.message',
                      'content': {
                        'msgtype': 'm.text',
                        'body': '* foxies',
                        'm.new_content': {
                          'msgtype': 'm.text',
                          'body': 'foxies',
                        },
                        'm.relates_to': {
                          'rel_type': 'm.replace',
                          'event_id': '\$last:example.com',
                        },
                      },
                      'origin_server_ts': 1417731086799,
                      'event_id': '\$edit2:example.com',
                    }
                  ],
                },
              },
            },
          },
        }),
      );
      expect(room.lastEvent!.content['body'], '* foxies');
    });
    test('set prev_batch when invite then join', () async {
      await matrix.handleSync(
        SyncUpdate.fromJson({
          'next_batch': '1',
          'rooms': {
            'invite': {
              'new_room_id': {
                'invite_state': {
                  'events': [
                    MatrixEvent(
                      eventId: '0',
                      type: EventTypes.RoomCreate,
                      content: {'body': '0'},
                      senderId: '@alice:example.com',
                      stateKey: '@alice:example.com',
                      originServerTs: DateTime.now(),
                    ).toJson(),
                  ],
                },
              },
            },
          },
        }),
      );
      expect(
        matrix.rooms.singleWhereOrNull(
          (element) => element.id == 'new_room_id',
        ),
        isNotNull,
      );
      final newRoom = matrix.getRoomById('new_room_id')!;
      expect(newRoom.prev_batch, null);
      await matrix.handleSync(
        SyncUpdate.fromJson({
          'next_batch': '3',
          'rooms': {
            'join': {
              'new_room_id': {
                'timeline': {
                  'events': [
                    MatrixEvent(
                      eventId: '2',
                      type: EventTypes.RoomCreate,
                      content: {'body': '2'},
                      senderId: '@alice:example.com',
                      stateKey: '@alice:example.com',
                      originServerTs: DateTime.now(),
                    ).toJson(),
                  ],
                  'prev_batch': '1',
                },
              },
            },
          },
        }),
      );
      final timeline = await newRoom.getTimeline();
      expect(newRoom.prev_batch, '1');
      expect(timeline.events.length, 1);
      expect(timeline.events.first.eventId, '2');
      await matrix.handleSync(
        SyncUpdate.fromJson({
          'next_batch': '4',
          'rooms': {
            'join': {
              'new_room_id': {
                'timeline': {
                  'events': [
                    MatrixEvent(
                      eventId: '3',
                      type: EventTypes.Message,
                      content: {'body': '3'},
                      senderId: '@alice2:example.com',
                      stateKey: '@alice2:example.com',
                      originServerTs: DateTime.now(),
                    ).toJson(),
                  ],
                  'prev_batch': '2',
                },
              },
            },
          },
        }),
      );
      expect(newRoom.prev_batch, '1');
      expect(timeline.events.length, 2);
      expect(timeline.events.last.eventId, '2');
      expect(timeline.events.first.eventId, '3');
      if (timeline.canRequestHistory) {
        await timeline.requestHistory();
      }
      expect(newRoom.prev_batch, 'emptyHistoryResponse');
      expect(timeline.events.length, 3);
      expect(timeline.events.last.eventId, '0');
      expect(timeline.events.first.eventId, '3');
      while (timeline.canRequestHistory) {
        await timeline.requestHistory();
      }
      expect(newRoom.prev_batch, null);
      expect(timeline.events.length, 3);
      expect(timeline.events.last.eventId, '0');
      expect(timeline.events.first.eventId, '3');
    });
    test('getProfileFromUserId', () async {
      final cachedProfile = await matrix.getUserProfile('@getme:example.com');
      expect(cachedProfile.outdated, false);
      expect(cachedProfile.avatarUrl.toString(), 'mxc://test');
      expect(cachedProfile.displayname, 'You got me');
      final profile = await matrix.getProfileFromUserId('@getme:example.com');
      expect(profile.avatarUrl.toString(), 'mxc://test');
      expect(profile.displayName, 'You got me');
      final aliceProfile = await matrix.getUserProfile('@alice:example.com');
      expect(aliceProfile.outdated, false);
      expect(aliceProfile.avatarUrl.toString(), 'mxc://test');
      expect(aliceProfile.displayname, 'Alice M');
      await matrix.handleSync(
        SyncUpdate(
          nextBatch: '',
          rooms: RoomsUpdate(
            join: {
              matrix.rooms.first.id: JoinedRoomUpdate(
                timeline: TimelineUpdate(
                  events: [
                    MatrixEvent(
                      eventId: 'abcd',
                      type: EventTypes.RoomMember,
                      content: {'membership': 'join'},
                      senderId: '@alice:example.com',
                      stateKey: '@alice:example.com',
                      originServerTs: DateTime.now(),
                    ),
                  ],
                ),
              ),
            },
          ),
        ),
      );
      expect(matrix.onUserProfileUpdate.value, '@alice:example.com');
      final cachedProfileFromDb =
          await matrix.database?.getUserProfile('@alice:example.com');
      expect(cachedProfileFromDb?.outdated, true);
    });
    test('joinAfterInviteMembership', () async {
      final client = await getClient();
      await client.abortSync();
      client.rooms.clear();
      await client.database?.clearCache();
      await client.handleSync(
        SyncUpdate.fromJson(
          jsonDecode(
            '{"next_batch":"s198510_227245_8_1404_23586_11_51065_267416_0_2639","rooms":{"invite":{"!bWEUQDujMKwjxkCXYr:tim-alpha.staging.famedly.de":{"invite_state":{"events":[{"type":"m.room.create","content":{"type":"de.gematik.tim.roomtype.default.v1","room_version":"10","creator":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":""},{"type":"m.room.encryption","content":{"algorithm":"m.megolm.v1.aes-sha2"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":""},{"type":"m.room.join_rules","content":{"join_rule":"invite"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":""},{"type":"m.room.member","content":{"membership":"join","displayname":"Tóboggen, Veronika Freifrau"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de"},{"type":"m.room.member","content":{"is_direct":true,"membership":"invite","displayname":"Düsterbehn-Hardenbergshausen, Michael von"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de"}]}}}},"presence":{"events":[{"type":"m.presence","content":{"presence":"online","last_active_ago":5948,"currently_active":true},"sender":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de"}]},"device_one_time_keys_count":{"signed_curve25519":66},"device_unused_fallback_key_types":["signed_curve25519"],"org.matrix.msc2732.device_unused_fallback_key_types":["signed_curve25519"]}',
          ),
        ),
      );
      await client.handleSync(
        SyncUpdate.fromJson(
          jsonDecode(
            '{"next_batch":"s198511_227245_8_1404_23588_11_51066_267416_0_2639","account_data":{"events":[{"type":"m.direct","content":{"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de":["!bWEUQDujMKwjxkCXYr:tim-alpha.staging.famedly.de"]}}]},"device_one_time_keys_count":{"signed_curve25519":65},"device_unused_fallback_key_types":["signed_curve25519"],"org.matrix.msc2732.device_unused_fallback_key_types":["signed_curve25519"]}',
          ),
        ),
      );
      await client.handleSync(
        SyncUpdate.fromJson(
          jsonDecode(
            '{"next_batch":"s198512_227245_8_1404_23588_11_51066_267416_0_2639","rooms":{"join":{"!bWEUQDujMKwjxkCXYr:tim-alpha.staging.famedly.de":{"summary":{"m.heroes":["@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de"],"m.joined_member_count":2,"m.invited_member_count":0},"state":{"events":[{"type":"m.room.create","content":{"type":"de.gematik.tim.roomtype.default.v1","room_version":"10","creator":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"","event_id":"\$qSgXGXjly6p5Kwbdb_PMBC_EF7nzHDbM23mvJFVeoiE","origin_server_ts":1709565579735,"unsigned":{"age":2255}}]},"timeline":{"events":[{"type":"m.room.member","content":{"membership":"join","displayname":"Tóboggen, Veronika Freifrau"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","event_id":"\$rQzxxTrSd9Y0koxIGlkalPAV_lwu94jLOA-8PSunY24","origin_server_ts":1709565579871,"unsigned":{"age":2119}},{"type":"m.room.power_levels","content":{"users":{"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de":100,"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de":100},"users_default":0,"events":{"m.room.name":50,"m.room.power_levels":100,"m.room.history_visibility":100,"m.room.canonical_alias":50,"m.room.avatar":50,"m.room.tombstone":100,"m.room.server_acl":100,"m.room.encryption":100},"events_default":0,"state_default":50,"ban":50,"kick":50,"redact":50,"invite":0,"historical":100},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"","event_id":"\$d6sgGs8PmkAbC3Iw3CkPT1QSub2zFTTvytegOxkPYPs","origin_server_ts":1709565579966,"unsigned":{"age":2024}},{"type":"m.room.join_rules","content":{"join_rule":"invite"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"","event_id":"\$EnA2Podch5181X4G1ZX34zaFGS_V4ZCZzLkBEfS_qyg","origin_server_ts":1709565579979,"unsigned":{"age":2011}},{"type":"m.room.history_visibility","content":{"history_visibility":"shared"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"","event_id":"\$6tNNo6ZkpZZrHrn8ZjXhMqI0CNv-VNNBw4R0h3_O-Tc","origin_server_ts":1709565579979,"unsigned":{"age":2011}},{"type":"m.room.guest_access","content":{"guest_access":"can_join"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"","event_id":"\$ViuL_LpN1sY9oYcGwycNjtp6FcGj__smUg8mzj3oa2o","origin_server_ts":1709565579980,"unsigned":{"age":2010}},{"type":"m.room.encryption","content":{"algorithm":"m.megolm.v1.aes-sha2"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"","event_id":"\$_e0az7OP7D78QU7DItiRAtlHlZmA07B5wenR93x5V1E","origin_server_ts":1709565579981,"unsigned":{"age":2009}},{"type":"m.room.member","content":{"is_direct":true,"membership":"invite","displayname":"Düsterbehn-Hardenbergshausen, Michael von"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","state_key":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de","event_id":"\$4sZ3CF67SUh0n5WG0ZKS47Epj9B_d842RJjnrQmUKQo","origin_server_ts":1709565580185,"unsigned":{"age":1805}},{"type":"m.room.notsoencrypted","content":{"algorithm":"m.megolm.v1.aes-sha2","ciphertext":"AwgAEpACEZw8Ymg99Yfl7VsXRIdczlQ3+YSJ6te3o6ka/XXP0h4ZsgR2bu1Q8puQ77fOpwX5dPnrrCi5SQg9Zv5/u+0QbFV4FKE/k03Vxao/tiswb6wST14x9kYkwViOrZe7fzg7VF9tCi8U88TqxGsPDDOVjO+WNxG8I9ldP1zvPsxYzVSyGPhaB5E+q6llwlXcQ56wvpf7Ke7gX4Ly2Dlxa8Bmy7aUSCBoWAt/xFRdzCOsE9qI8oxzuvk4RF0H/7bY+4DkGTsP1rIYgA7Q0JueIFb47Yu6pK26BCKo1yPAR8qvpe8vGBICm4slMbKaJN4RqBHtR0zc12E5DXud91o3mArqTksv1NEbI1F4XgDREl76WBw8a7MafDSuun09JuWpGxzPHvLVOUVny6tTJPRutsZLkmnTeMTiXnsPexUiY7UTYlzOMeeoUSTDuJXJz6CM+gSc52CiKoHK/gE","device_id":"TNLOYXJFXM","sender_key":"e9W0gpUcSEKOQ8P/xIdroHUpP7yG4EjQfueiAngESRk","session_id":"hhZ8TBs9Xp0dmuvC6XpDBYsAKnTqb8WiBhZMzHcbBXI"},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","event_id":"\$KKIZX8cuB3S3uzS7CDtRlTkcaJRW73e2HW2NuW6OTEg","origin_server_ts":1709565580991,"unsigned":{"age":999}},{"type":"m.room.member","content":{"membership":"join","displayname":"Düsterbehn-Hardenbergshausen, Michael von"},"sender":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de","state_key":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de","event_id":"\$UNSLEyhC_93oQlt0tWoai4CCd3LH2GJJexM0WN2wxCA","origin_server_ts":1709565581813,"unsigned":{"replaces_state":"\$4sZ3CF67SUh0n5WG0ZKS47Epj9B_d842RJjnrQmUKQo","prev_content":{"is_direct":true,"membership":"invite","displayname":"Düsterbehn-Hardenbergshausen, Michael von"},"prev_sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","age":177}},{"type":"m.room.member","content":{"membership":"join","displayname":"Düsterbehn-Hardenbergshausen, Michael von"},"sender":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de","state_key":"@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de","event_id":"\$UNSLEyhC_93oQlt0tWoai4CCd3LH2GJJexM0WN2wxCA","origin_server_ts":1709565581813,"unsigned":{"replaces_state":"\$4sZ3CF67SUh0n5WG0ZKS47Epj9B_d842RJjnrQmUKQo","prev_content":{"is_direct":true,"membership":"invite","displayname":"Düsterbehn-Hardenbergshausen, Michael von"},"prev_sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de","age":177}}],"limited":true,"prev_batch":"s198503_227245_8_1404_23588_11_51066_267416_0_2639"},"ephemeral":{"events":[]},"account_data":{"events":[]},"unread_notifications":{"highlight_count":0,"notification_count":0}}}},"presence":{"events":[{"type":"m.presence","content":{"presence":"online","last_active_ago":843,"currently_active":true},"sender":"@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de"}]},"device_lists":{"changed":["@duesterbehn-hardenbergshausen_michael_von:tim-alpha.staging.famedly.de","@toboggen_veronika_freifrau:tim-alpha.staging.famedly.de"],"left":[]},"device_one_time_keys_count":{"signed_curve25519":65},"device_unused_fallback_key_types":["signed_curve25519"],"org.matrix.msc2732.device_unused_fallback_key_types":["signed_curve25519"]}',
          ),
        ),
      );
      //await client.handleSync(SyncUpdate.fromJson(jsonDecode('')));
      final room = client
          .getRoomById('!bWEUQDujMKwjxkCXYr:tim-alpha.staging.famedly.de')!;
      await room.postLoad();
      final participants = await room.requestParticipants();
      expect(
        participants.where((u) => u.membership == Membership.join).length,
        2,
      );
      await client.abortSync();
      client.rooms.clear();
      await client.database?.clearCache();
      await client.dispose(closeDatabase: true);
    });
    test('leaveThenInvite should be invited', () async {
      // Synapse includes a room in both invite and leave if you leave and get
      // reinvited while you are offline. The other direction only contains the
      // room in leave. Verify that we actually store the invite in the first
      // case. See also
      // https://github.com/famedly/product-management/issues/2283
      final client = await getClient();
      await client.abortSync();
      client.rooms.clear();
      await client.database?.clearCache();
      final roomId = '!inviteLeaveRoom:example.com';
      await client.handleSync(
        SyncUpdate(
          nextBatch: 'ABCDEF',
          rooms: RoomsUpdate(
            invite: {
              roomId: InvitedRoomUpdate(
                inviteState: [
                  StrippedStateEvent(
                    type: EventTypes.RoomMember,
                    senderId: '@bob:example.com',
                    stateKey: client.userID,
                    content: {
                      'membership': 'invite',
                    },
                  ),
                ],
              ),
            },
            leave: {
              roomId: LeftRoomUpdate(
                state: [
                  MatrixEvent(
                    type: EventTypes.RoomMember,
                    senderId: client.userID!,
                    stateKey: client.userID,
                    originServerTs: DateTime.now(),
                    eventId:
                        '\$abcdefwsjaskdfabsjfhabfsjgbahsjfkgbasjffsajfgsfd',
                    content: {
                      'membership': 'leave',
                    },
                  ),
                ],
              ),
            },
          ),
        ),
      );
      final room = client.getRoomById(roomId);
      expect(room?.membership, Membership.invite);
      await client.abortSync();
      client.rooms.clear();
      await client.database?.clearCache();
      await client.dispose(closeDatabase: true);
    });
    test('ownProfile', () async {
      final client = await getClient();
      await client.abortSync();
      client.rooms.clear();
      await client.database?.clearCache();
      await client.handleSync(
        SyncUpdate.fromJson(
          jsonDecode(
            '{"next_batch":"s82_571_2_6_39_1_2_34_1","account_data":{"events":[{"type":"m.push_rules","content":{"global":{"underride":[{"conditions":[{"kind":"event_match","key":"type","pattern":"m.call.invite"}],"actions":["notify",{"set_tweak":"sound","value":"ring"},{"set_tweak":"highlight","value":false}],"rule_id":".m.rule.call","default":true,"enabled":true},{"conditions":[{"kind":"room_member_count","is":"2"},{"kind":"event_match","key":"type","pattern":"m.room.message"}],"actions":["notify",{"set_tweak":"sound","value":"default"},{"set_tweak":"highlight","value":false}],"rule_id":".m.rule.room_one_to_one","default":true,"enabled":true},{"conditions":[{"kind":"room_member_count","is":"2"},{"kind":"event_match","key":"type","pattern":"m.room.encrypted"}],"actions":["notify",{"set_tweak":"sound","value":"default"},{"set_tweak":"highlight","value":false}],"rule_id":".m.rule.encrypted_room_one_to_one","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"m.room.message"}],"actions":["notify",{"set_tweak":"highlight","value":false}],"rule_id":".m.rule.message","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"m.room.encrypted"}],"actions":["notify",{"set_tweak":"highlight","value":false}],"rule_id":".m.rule.encrypted","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"im.vector.modular.widgets"},{"kind":"event_match","key":"content.type","pattern":"jitsi"},{"kind":"event_match","key":"state_key","pattern":"*"}],"actions":["notify",{"set_tweak":"highlight","value":false}],"rule_id":".im.vector.jitsi","default":true,"enabled":true}],"sender":[],"room":[],"content":[{"actions":["notify",{"set_tweak":"sound","value":"default"},{"set_tweak":"highlight"}],"pattern":"056d6976-fb61-47cf-86f0-147387461565","rule_id":".m.rule.contains_user_name","default":true,"enabled":true}],"override":[{"conditions":[],"actions":["dont_notify"],"rule_id":".m.rule.master","default":true,"enabled":false},{"conditions":[{"kind":"event_match","key":"content.msgtype","pattern":"m.notice"}],"actions":["dont_notify"],"rule_id":".m.rule.suppress_notices","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"m.room.member"},{"kind":"event_match","key":"content.membership","pattern":"invite"},{"kind":"event_match","key":"state_key","pattern":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de"}],"actions":["notify",{"set_tweak":"sound","value":"default"},{"set_tweak":"highlight","value":false}],"rule_id":".m.rule.invite_for_me","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"m.room.member"}],"actions":["dont_notify"],"rule_id":".m.rule.member_event","default":true,"enabled":true},{"conditions":[{"kind":"contains_display_name"}],"actions":["notify",{"set_tweak":"sound","value":"default"},{"set_tweak":"highlight"}],"rule_id":".m.rule.contains_display_name","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"content.body","pattern":"@room"},{"kind":"sender_notification_permission","key":"room"}],"actions":["notify",{"set_tweak":"highlight","value":true}],"rule_id":".m.rule.roomnotif","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"m.room.tombstone"},{"kind":"event_match","key":"state_key","pattern":""}],"actions":["notify",{"set_tweak":"highlight","value":true}],"rule_id":".m.rule.tombstone","default":true,"enabled":true},{"conditions":[{"kind":"event_match","key":"type","pattern":"m.reaction"}],"actions":["dont_notify"],"rule_id":".m.rule.reaction","default":true,"enabled":true}]},"device":{}}}]},"presence":{"events":[{"type":"m.presence","sender":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"presence":"online","last_active_ago":43,"currently_active":true}}]},"device_one_time_keys_count":{"signed_curve25519":66},"org.matrix.msc2732.device_unused_fallback_key_types":["signed_curve25519"],"device_unused_fallback_key_types":["signed_curve25519"],"rooms":{"join":{"!MEgZosbiZqjSjbHFqI:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de":{"timeline":{"events":[{"type":"m.room.member","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"membership":"join","displayname":"Lars Kaiser"},"state_key":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","origin_server_ts":1647296944593,"unsigned":{"age":545455},"event_id":"\$mk9kFUEAKBZJgarWApLyYqOZQQocLIVV8tWp_gJEZFU"},{"type":"m.room.power_levels","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"users":{"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de":100},"users_default":0,"events":{"m.room.name":50,"m.room.power_levels":100,"m.room.history_visibility":100,"m.room.canonical_alias":50,"m.room.avatar":50,"m.room.tombstone":100,"m.room.server_acl":100,"m.room.encryption":100},"events_default":0,"state_default":50,"ban":50,"kick":50,"redact":50,"invite":50,"historical":100},"state_key":"","origin_server_ts":1647296944690,"unsigned":{"age":545358},"event_id":"\$3wL2YgVNQzgfl8y_ksi3BPMqRs94jb_m0WRonL1HNpY"},{"type":"m.room.canonical_alias","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"alias":"#user-discovery:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de"},"state_key":"","origin_server_ts":1647296944806,"unsigned":{"age":545242},"event_id":"\$yXaVETL9F4jSN9rpRNyT_kUoctzD07n5Z4AIHziP7DQ"},{"type":"m.room.join_rules","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"join_rule":"public"},"state_key":"","origin_server_ts":1647296944894,"unsigned":{"age":545154},"event_id":"\$jBDHhgpNqr125eWUsGVw4r7ZG2hgr0BTzzR77S-ubvY"},{"type":"m.room.history_visibility","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"history_visibility":"shared"},"state_key":"","origin_server_ts":1647296944965,"unsigned":{"age":545083},"event_id":"\$kMessP7gAphUKW7mzOLlJT6NT8IsVGPmGir3_1uBNCE"},{"type":"m.room.name","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"name":"User Discovery"},"state_key":"","origin_server_ts":1647296945062,"unsigned":{"age":544986},"event_id":"\$Bo9Ut_0vcr3FuxCRye4IHEMxUxIIcSwc-ePnMzx-hYU"},{"type":"m.room.member","sender":"@test:fakeServer.notExisting","content":{"membership":"join","displayname":"1c2e5c2b-f958-45a5-9fcb-eef3969c31df"},"state_key":"@test:fakeServer.notExisting","origin_server_ts":1647296989893,"unsigned":{"age":500155},"event_id":"\$fYCf2qtlHwzcdLgwjHb2EOdStv3isAlIUy2Esh5qfVE"},{"type":"m.room.member","sender":"@test:fakeServer.notExisting","content":{"membership":"join","displayname":"Some First Name Some Last Name"},"state_key":"@test:fakeServer.notExisting","origin_server_ts":1647296990076,"unsigned":{"replaces_state":"\$fYCf2qtlHwzcdLgwjHb2EOdStv3isAlIUy2Esh5qfVE","prev_content":{"membership":"join","displayname":"1c2e5c2b-f958-45a5-9fcb-eef3969c31df"},"prev_sender":"@test:fakeServer.notExisting","age":499972},"event_id":"\$3Ut97nFBgOtsrnRPW-pqr28z7ETNMttj7GcjkIv4zWw"},{"type":"m.room.member","sender":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"membership":"join","displayname":"056d6976-fb61-47cf-86f0-147387461565"},"state_key":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","origin_server_ts":1647297489154,"unsigned":{"age":894},"event_id":"\$6EsjHSLQDVDW9WDH1c5Eu57VaPGZmOPtNRjCjtWPLV0"},{"type":"m.room.member","sender":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"membership":"join","displayname":"Another User"},"state_key":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","origin_server_ts":1647297489290,"unsigned":{"replaces_state":"\$6EsjHSLQDVDW9WDH1c5Eu57VaPGZmOPtNRjCjtWPLV0","prev_content":{"membership":"join","displayname":"056d6976-fb61-47cf-86f0-147387461565"},"prev_sender":"@056d6976-fb61-47cf-86f0-147387461565:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","age":758},"event_id":"\$dtQblqCbjr3TGc3WmrQ4YTkHaXJ2PcO0TAYDr9K7iQc"}],"prev_batch":"t2-62_571_2_6_39_1_2_34_1","limited":true},"state":{"events":[{"type":"m.room.create","sender":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de","content":{"m.federate":false,"room_version":"9","creator":"@8640f1e6-a824-4f9c-9924-2d8fc40bc030:c3d35860-36fe-45d1-8e16-936cf50513fb.gedisa-staging.famedly.de"},"state_key":"","origin_server_ts":1647296944511,"unsigned":{"age":545537},"event_id":"\$PAWKKULBVOLnqfrAAtXZz8tHEPXXjgRVbJJLifwQWbE"}]},"account_data":{"events":[]},"ephemeral":{"events":[]},"unread_notifications":{"notification_count":0,"highlight_count":0},"summary":{"m.joined_member_count":3,"m.invited_member_count":0},"org.matrix.msc2654.unread_count":0}}}}',
          ),
        ),
      );
      final profile = await client.getUserProfile(client.userID!);
      expect(profile.displayname, 'Some First Name Some Last Name');
      expect(profile.outdated, false);
      await client.dispose(closeDatabase: true);
    });
    test('sendToDeviceEncrypted', () async {
      FakeMatrixApi.calledEndpoints.clear();
      await matrix.sendToDeviceEncrypted(
          matrix.userDeviceKeys['@alice:example.com']!.deviceKeys.values
              .toList(),
          'm.message',
          {
            'msgtype': 'm.text',
            'body': 'Hello world',
          });
      expect(
        FakeMatrixApi.calledEndpoints.keys.any(
          (k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
        ),
        true,
      );
    });
    test('sendToDeviceEncryptedChunked', () async {
      FakeMatrixApi.calledEndpoints.clear();
      await matrix.sendToDeviceEncryptedChunked(
          matrix.userDeviceKeys['@alice:example.com']!.deviceKeys.values
              .toList(),
          'm.message',
          {
            'msgtype': 'm.text',
            'body': 'Hello world',
          });
      await Future.delayed(Duration(milliseconds: 100));
      expect(
        FakeMatrixApi.calledEndpoints.keys
            .where(
              (k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
            )
            .length,
        1,
      );
      final deviceKeys = [];
      for (var i = 0; i < 30; i++) {
        final account = olm.Account();
        account.create();
        final keys = json.decode(account.identity_keys());
        final userId = '@testuser:example.org';
        final deviceId = 'DEVICE$i';
        final keyObj = {
          'user_id': userId,
          'device_id': deviceId,
          'algorithms': [
            'm.olm.v1.curve25519-aes-sha2',
            'm.megolm.v1.aes-sha2',
          ],
          'keys': {
            'curve25519:$deviceId': keys['curve25519'],
            'ed25519:$deviceId': keys['ed25519'],
          },
        };
        final signature =
            account.sign(String.fromCharCodes(canonicalJson.encode(keyObj)));
        keyObj['signatures'] = {
          userId: {
            'ed25519:$deviceId': signature,
          },
        };
        account.free();
        deviceKeys.add(DeviceKeys.fromJson(keyObj, matrix));
      }
      FakeMatrixApi.calledEndpoints.clear();
      await matrix.sendToDeviceEncryptedChunked(deviceKeys, 'm.message', {
        'msgtype': 'm.text',
        'body': 'Hello world',
      });
      // it should send the first chunk right away
      expect(
        FakeMatrixApi.calledEndpoints.keys
            .where(
              (k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
            )
            .length,
        1,
      );
      await Future.delayed(Duration(milliseconds: 100));
      expect(
        FakeMatrixApi.calledEndpoints.keys
            .where(
              (k) => k.startsWith('/client/v3/sendToDevice/m.room.encrypted'),
            )
            .length,
        2,
      );
    });
    test('send to_device queue', () async {
      // we test:
      // send fox --> fail
      // send raccoon --> fox & raccoon sent
      // send bunny --> only bunny sent
      final client = await getClient();
      FakeMatrixApi.failToDevice = true;
      final foxContent = {
        '@fox:example.org': {
          '*': {
            'fox': 'hole',
          },
        },
      };
      final raccoonContent = {
        '@fox:example.org': {
          '*': {
            'raccoon': 'mask',
          },
        },
      };
      final bunnyContent = {
        '@fox:example.org': {
          '*': {
            'bunny': 'burrow',
          },
        },
      };
      await client
          .sendToDevice('foxies', 'floof_txnid', foxContent)
          .catchError((e) => null); // ignore the error
      FakeMatrixApi.failToDevice = false;
      FakeMatrixApi.calledEndpoints.clear();
      await client.sendToDevice('raccoon', 'raccoon_txnid', raccoonContent);
      expect(
        json.decode(
          FakeMatrixApi
                  .calledEndpoints['/client/v3/sendToDevice/foxies/floof_txnid']
              ?[0],
        )['messages'],
        foxContent,
      );
      expect(
        json.decode(
          FakeMatrixApi.calledEndpoints[
              '/client/v3/sendToDevice/raccoon/raccoon_txnid']?[0],
        )['messages'],
        raccoonContent,
      );
      FakeMatrixApi.calledEndpoints.clear();
      await client.sendToDevice('bunny', 'bunny_txnid', bunnyContent);
      expect(
        FakeMatrixApi
            .calledEndpoints['/client/v3/sendToDevice/foxies/floof_txnid'],
        null,
      );
      expect(
        FakeMatrixApi
            .calledEndpoints['/client/v3/sendToDevice/raccoon/raccoon_txnid'],
        null,
      );
      expect(
        json.decode(
          FakeMatrixApi
              .calledEndpoints['/client/v3/sendToDevice/bunny/bunny_txnid']?[0],
        )['messages'],
        bunnyContent,
      );
      await client.dispose(closeDatabase: true);
    });
    test('send to_device queue multiple', () async {
      // we test:
      // send fox --> fail
      // send raccoon --> fail
      // send bunny --> all sent
      final client = await getClient();
      await client.abortSync();
      FakeMatrixApi.failToDevice = true;
      final foxContent = {
        '@fox:example.org': {
          '*': {
            'fox': 'hole',
          },
        },
      };
      final raccoonContent = {
        '@fox:example.org': {
          '*': {
            'raccoon': 'mask',
          },
        },
      };
      final bunnyContent = {
        '@fox:example.org': {
          '*': {
            'bunny': 'burrow',
          },
        },
      };
      await client
          .sendToDevice('foxies', 'floof_txnid', foxContent)
          .catchError((e) => null); // ignore the error
      await FakeMatrixApi.firstWhereValue(
        '/client/v3/sendToDevice/foxies/floof_txnid',
      );
      FakeMatrixApi.calledEndpoints.clear();
      await client
          .sendToDevice('raccoon', 'raccoon_txnid', raccoonContent)
          .catchError((e) => null);
      await FakeMatrixApi.firstWhereValue(
        '/client/v3/sendToDevice/foxies/floof_txnid',
      );
      FakeMatrixApi.calledEndpoints.clear();
      FakeMatrixApi.failToDevice = false;
      await client.sendToDevice('bunny', 'bunny_txnid', bunnyContent);
      await FakeMatrixApi.firstWhereValue(
        '/client/v3/sendToDevice/foxies/floof_txnid',
      );
      await FakeMatrixApi.firstWhereValue(
        '/client/v3/sendToDevice/bunny/bunny_txnid',
      );
      final foxcall = FakeMatrixApi
          .calledEndpoints['/client/v3/sendToDevice/foxies/floof_txnid']?[0];
      expect(foxcall != null, true);
      expect(json.decode(foxcall)['messages'], foxContent);
      final racooncall = FakeMatrixApi
          .calledEndpoints['/client/v3/sendToDevice/raccoon/raccoon_txnid']?[0];
      expect(racooncall != null, true);
      expect(json.decode(racooncall)['messages'], raccoonContent);
      final bunnycall = FakeMatrixApi
          .calledEndpoints['/client/v3/sendToDevice/bunny/bunny_txnid']?[0];
      expect(bunnycall != null, true);
      expect(json.decode(bunnycall)['messages'], bunnyContent);
      await client.dispose(closeDatabase: true);
    });
    test('startDirectChat', () async {
      await matrix.startDirectChat('@alice:example.com', waitForSync: false);
      expect(
        json.decode(
          FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.last,
        ),
        {
          'initial_state': [
            {
              'content': {'algorithm': 'm.megolm.v1.aes-sha2'},
              'type': 'm.room.encryption',
            }
          ],
          'invite': ['@alice:example.com'],
          'is_direct': true,
          'preset': 'trusted_private_chat',
        },
      );
    });
    test('createGroupChat', () async {
      await matrix.createGroupChat(groupName: 'Testgroup', waitForSync: false);
      expect(
        json.decode(
          FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.last,
        ),
        {
          'initial_state': [
            {
              'content': {'algorithm': 'm.megolm.v1.aes-sha2'},
              'type': 'm.room.encryption',
            }
          ],
          'name': 'Testgroup',
          'preset': 'private_chat',
        },
      );
      await matrix.createGroupChat(
        groupName: 'Testgroup',
        waitForSync: false,
        groupCall: true,
        powerLevelContentOverride: {'events_default': 12},
      );
      expect(
        json.decode(
          FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.last,
        ),
        {
          'initial_state': [
            {
              'content': {'algorithm': 'm.megolm.v1.aes-sha2'},
              'type': 'm.room.encryption',
            }
          ],
          'name': 'Testgroup',
          'power_level_content_override': {
            'events_default': 12,
            'events': {'com.famedly.call.member': 12},
          },
          'preset': 'private_chat',
        },
      );
      await matrix.createGroupChat(
        groupName: 'Testgroup',
        waitForSync: false,
        groupCall: true,
        powerLevelContentOverride: {
          'events_default': 12,
          'events': {'com.famedly.call.member': 14},
        },
      );
      expect(
        json.decode(
          FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.last,
        ),
        {
          'initial_state': [
            {
              'content': {'algorithm': 'm.megolm.v1.aes-sha2'},
              'type': 'm.room.encryption',
            }
          ],
          'name': 'Testgroup',
          'power_level_content_override': {
            'events_default': 12,
            'events': {'com.famedly.call.member': 14},
          },
          'preset': 'private_chat',
        },
      );
      await matrix.createGroupChat(
        groupName: 'Testgroup',
        waitForSync: false,
        groupCall: true,
      );
      expect(
        json.decode(
          FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.last,
        ),
        {
          'initial_state': [
            {
              'content': {'algorithm': 'm.megolm.v1.aes-sha2'},
              'type': 'm.room.encryption',
            }
          ],
          'name': 'Testgroup',
          'power_level_content_override': {
            'events': {'com.famedly.call.member': 0},
          },
          'preset': 'private_chat',
        },
      );
    });
    test('Test the fake store api', () async {
      final database = await getDatabase(null);
      final client1 = Client(
        'testclient',
        httpClient: FakeMatrixApi(),
        databaseBuilder: (_) => database,
      );
      await client1.init(
        newToken: 'abc123',
        newUserID: '@test:fakeServer.notExisting',
        newHomeserver: Uri.parse('https://fakeServer.notExisting'),
        newDeviceName: 'Text Matrix Client',
        newDeviceID: 'GHTYAJCE',
        newOlmAccount: pickledOlmAccount,
      );
      await client1.abortSync();
      expect(client1.isLogged(), true);
      expect(client1.rooms.length, 3);
      final client2 = Client(
        'testclient',
        httpClient: FakeMatrixApi(),
        databaseBuilder: (_) => database,
      );
      await client2.init();
      await client2.abortSync();
      expect(client2.isLogged(), true);
      expect(client2.accessToken, client1.accessToken);
      expect(client2.userID, client1.userID);
      expect(client2.homeserver, client1.homeserver);
      expect(client2.deviceID, client1.deviceID);
      expect(client2.deviceName, client1.deviceName);
      expect(client2.rooms.length, 3);
      if (client2.encryptionEnabled) {
        expect(
          client2.encryption?.fingerprintKey,
          client1.encryption?.fingerprintKey,
        );
        expect(
          client2.encryption?.identityKey,
          client1.encryption?.identityKey,
        );
        expect(client2.rooms[1].id, client1.rooms[1].id);
      }
      await client1.logout();
      await client2.logout();
    });
    test('changePassword', () async {
      await matrix.changePassword('1234', oldPassword: '123456');
    });
    test('ignoredUsers', () async {
      expect(matrix.ignoredUsers, []);
      matrix.accountData['m.ignored_user_list'] = BasicEvent(
        type: 'm.ignored_user_list',
        content: {
          'ignored_users': {
            '@charley:stupid.abc': {},
          },
        },
      );
      expect(matrix.ignoredUsers, ['@charley:stupid.abc']);
      await matrix.ignoreUser('@charley2:stupid.abc');
      await matrix.unignoreUser('@charley:stupid.abc');
    });
    test('upload', () async {
      final client = await getClient();
      final response =
          await client.uploadContent(Uint8List(0), filename: 'file.jpeg');
      expect(response.toString(), 'mxc://example.com/AQwafuaFswefuhsfAFAgsw');
      expect(
        await client.database?.getFile(response) != null,
        client.database?.supportsFileStoring,
      );
      await client.dispose(closeDatabase: true);
    });
    test('wellKnown cache', () async {
      final client = await getClient();
      expect(client.wellKnown, null);
      await client.getWellknown();
      expect(
        client.wellKnown?.mHomeserver.baseUrl.host,
        'fakeserver.notexisting',
      );
      await client.dispose();
    });
    test('refreshAccessToken', () async {
      final client = await getClient();
      expect(client.accessToken, 'abcd');
      await client.refreshAccessToken();
      expect(client.accessToken, 'a_new_token');
      await client.dispose();
    });
    test('handleSoftLogout', () async {
      final client = await getClient();
      expect(client.accessToken, 'abcd');
      var softLoggedOut = 0;
      client.onSoftLogout = (client) {
        softLoggedOut++;
        return client.refreshAccessToken();
      };
      FakeMatrixApi.expectedAccessToken = 'a_new_token';
      await client.oneShotSync();
      await client.oneShotSync();
      FakeMatrixApi.expectedAccessToken = null;
      expect(client.accessToken, 'a_new_token');
      expect(softLoggedOut, 1);
      final storedClient = await client.database?.getClient(client.clientName);
      expect(storedClient?.tryGet('token'), 'a_new_token');
      expect(
        storedClient?.tryGet('refresh_token'),
        'another_new_token',
      );
      await client.dispose();
    });
    test('object equality', () async {
      final time1 = DateTime.fromMillisecondsSinceEpoch(1);
      final time2 = DateTime.fromMillisecondsSinceEpoch(0);
      final user1 =
          User('@user1:example.org', room: Room(id: '!room1', client: matrix));
      final user2 =
          User('@user2:example.org', room: Room(id: '!room1', client: matrix));
      // receipts
      expect(Receipt(user1, time1) == Receipt(user1, time1), true);
      expect(Receipt(user1, time1) == Receipt(user1, time2), false);
      expect(Receipt(user1, time1) == Receipt(user2, time1), false);
      // ignore: unrelated_type_equality_checks
      expect(Receipt(user1, time1) == 'beep', false);
      // users
      expect(user1 == user1, true);
      expect(user1 == user2, false);
      expect(
        user1 ==
            User(
              '@user1:example.org',
              room: Room(id: '!room2', client: matrix),
            ),
        false,
      );
      expect(
        user1 ==
            User(
              '@user1:example.org',
              room: Room(id: '!room1', client: matrix),
              membership: 'leave',
            ),
        false,
      );
      // ignore: unrelated_type_equality_checks
      expect(user1 == 'beep', false);
      // rooms
      expect(
        Room(id: '!room1', client: matrix) ==
            Room(id: '!room1', client: matrix),
        true,
      );
      expect(
        Room(id: '!room1', client: matrix) ==
            Room(id: '!room2', client: matrix),
        false,
      );
      // ignore: unrelated_type_equality_checks
      expect(Room(id: '!room1', client: matrix) == 'beep', false);
    });
    test('clearCache', () async {
      final client = await getClient();
      client.backgroundSync = true;
      await client.clearCache();
      await client.dispose();
    });
    test('dispose', () async {
      await matrix.dispose(closeDatabase: true);
    });
    test('Database Migration', () async {
      final firstDatabase = await getDatabase(null);
      final firstClient = Client(
        'testclient',
        httpClient: FakeMatrixApi(),
        databaseBuilder: (_) => firstDatabase,
      );
      FakeMatrixApi.client = firstClient;
      await firstClient.checkHomeserver(
        Uri.parse('https://fakeServer.notExisting'),
        checkWellKnown: false,
      );
      await firstClient.init(
        newToken: 'abcd',
        newUserID: '@test:fakeServer.notExisting',
        newHomeserver: firstClient.homeserver,
        newDeviceName: 'Text Matrix Client',
        newDeviceID: 'GHTYAJCE',
        newOlmAccount: pickledOlmAccount,
      );
      await Future.delayed(Duration(milliseconds: 200));
      await firstClient.dispose(closeDatabase: false);
      final newClient = Client(
        'testclient',
        httpClient: FakeMatrixApi(),
        databaseBuilder: getDatabase,
        legacyDatabaseBuilder: (_) => firstDatabase,
      );
      final Set initStates = {};
      await newClient.init(onInitStateChanged: initStates.add);
      expect(initStates, {
        InitState.initializing,
        InitState.migratingDatabase,
        InitState.settingUpEncryption,
        InitState.finished,
      });
      await Future.delayed(Duration(milliseconds: 200));
      expect(newClient.isLogged(), true);
      await newClient.dispose(closeDatabase: false);
      await firstDatabase.close();
      final sameOldFirstDatabase = await getDatabase(null);
      expect(await sameOldFirstDatabase.getClient('testclient'), null);
    });
    test('getEventByPushNotification', () async {
      final client = Client(
        'testclient',
        httpClient: FakeMatrixApi(),
        databaseBuilder: getDatabase,
      )
        ..accessToken = '1234'
        ..baseUri = Uri.parse('https://fakeserver.notexisting');
      Event? event;
      event = await client
          .getEventByPushNotification(PushNotification(devices: []));
      expect(event, null);
      event = await client.getEventByPushNotification(
        PushNotification(
          devices: [],
          eventId: '123',
          roomId: '!localpart2:server.abc',
          content: {
            'msgtype': 'm.text',
            'body': 'Hello world',
          },
          roomAlias: '#testalias:blaaa',
          roomName: 'TestRoomName',
          sender: '@alicyy:example.com',
          senderDisplayName: 'AlicE',
          type: 'm.room.message',
        ),
      );
      expect(event?.eventId, '123');
      expect(event?.body, 'Hello world');
      expect(event?.senderId, '@alicyy:example.com');
      expect(event?.senderFromMemoryOrFallback.calcDisplayname(), 'AlicE');
      expect(event?.type, 'm.room.message');
      expect(event?.messageType, 'm.text');
      expect(event?.room.id, '!localpart2:server.abc');
      expect(event?.room.name, 'TestRoomName');
      expect(event?.room.canonicalAlias, '#testalias:blaaa');
      final storedEvent =
          await client.database?.getEventById('123', event!.room);
      expect(storedEvent?.eventId, event?.eventId);
      event = await client.getEventByPushNotification(
        PushNotification(
          devices: [],
          eventId: '1234',
          roomId: '!localpart:server.abc',
        ),
      );
      expect(event?.eventId, '143273582443PhrSn:example.org');
      expect(event?.room.id, '!localpart:server.abc');
      expect(event?.body, 'This is an example text message');
      expect(event?.messageType, 'm.text');
      expect(event?.type, 'm.room.message');
      final storedEvent2 = await client.database
          ?.getEventById('143273582443PhrSn:example.org', event!.room);
      expect(storedEvent2?.eventId, event?.eventId);
    });
    test('Rooms and archived rooms getter', () async {
      final client = await getClient();
      await Future.delayed(Duration(milliseconds: 50));
      expect(
        client.rooms.length,
        3,
        reason:
            'Count of invited+joined before loadArchive() rooms does not match',
      );
      expect(
        client.archivedRooms.length,
        0,
        reason: 'Count of archived rooms before loadArchive() does not match',
      );
      await client.loadArchive();
      expect(
        client.rooms.length,
        3,
        reason: 'Count of invited+joined rooms does not match',
      );
      expect(
        client.archivedRooms.length,
        2,
        reason: 'Count of archived rooms does not match',
      );
      expect(
        client.archivedRooms.firstWhereOrNull(
              (r) => r.room.id == '!5345234234:example.com',
            ) !=
            null,
        true,
        reason: '!5345234234:example.com not found as archived room',
      );
      expect(
        client.archivedRooms.firstWhereOrNull(
              (r) => r.room.id == '!5345234235:example.com',
            ) !=
            null,
        true,
        reason: '!5345234235:example.com not found as archived room',
      );
      await client.dispose();
    });
    test(
      'Client Init Exception',
      () async {
        final customClient = Client(
          'failclient',
          databaseBuilder: getMatrixSdkDatabase,
        );
        try {
          await customClient.init(
            newToken: 'testtoken',
            newDeviceID: 'testdeviceid',
            newDeviceName: 'testdevicename',
            newHomeserver: Uri.parse('https://test.server'),
            newOlmAccount: 'abcd',
            newUserID: '@user:server',
          );
          throw Exception('No exception?');
        } on ClientInitException catch (error) {
          expect(error.accessToken, 'testtoken');
          expect(error.deviceId, 'testdeviceid');
          expect(error.deviceName, 'testdevicename');
          expect(error.homeserver, Uri.parse('https://test.server'));
          expect(error.olmAccount, 'abcd');
          expect(error.userId, '@user:server');
          expect(error.toString(), 'Exception: BAD_ACCOUNT_KEY');
        }
        await customClient.dispose(closeDatabase: true);
      },
    );
    tearDown(() async {
      await matrix.dispose(closeDatabase: true);
    });
  });
}