import 'package:gametime_server/application/application.dart';
import 'package:gametime_server/domain/domain.dart';
import 'package:test/test.dart';

void main() {
  late _FakeSyncedResourceRepository resources;
  late _FakeClock clock;
  late _FakeIds ids;

  setUp(() {
    clock = _FakeClock(DateTime.utc(2026, 7, 19, 12));
    ids = _FakeIds();
    resources = _FakeSyncedResourceRepository();
  });

  test(
    'push inserts, updates newer items and ignores older or equal items',
    () async {
      resources.put(
        _resource(
          serverId: 'existing-server-id',
          resourceType: SyncedResourceType.exercise,
          clientId: 'exercise-1',
          clientUpdatedAt: DateTime.utc(2026, 7, 19, 10),
          serverUpdatedAt: DateTime.utc(2026, 7, 19, 11),
          payloadJson: {'name': 'old'},
        ),
      );
      final useCase = PushSyncUseCase(
        resources: resources,
        clock: clock,
        ids: ids,
      );

      final result = await useCase.execute(
        ownerUserId: 'user-1',
        deviceId: 'device-1',
        items: [
          PushSyncItemInput(
            resourceType: 'program',
            clientId: 'program-1',
            schemaVersion: 1,
            clientUpdatedAt: DateTime.utc(2026, 7, 19, 9),
            deletedAt: null,
            payloadJson: {'name': 'program'},
          ),
          PushSyncItemInput(
            resourceType: 'exercise',
            clientId: 'exercise-1',
            schemaVersion: 1,
            clientUpdatedAt: DateTime.utc(2026, 7, 19, 10),
            deletedAt: null,
            payloadJson: {'name': 'same'},
          ),
          PushSyncItemInput(
            resourceType: 'exercise',
            clientId: 'exercise-1',
            schemaVersion: 2,
            clientUpdatedAt: DateTime.utc(2026, 7, 19, 10, 1),
            deletedAt: null,
            payloadJson: {'name': 'new'},
          ),
          const PushSyncItemInput(
            resourceType: 'unknown',
            clientId: 'bad-1',
            schemaVersion: 1,
            clientUpdatedAt: null,
            deletedAt: null,
            payloadJson: {},
          ),
        ],
      );

      expect(result.results.map((item) => item.status), [
        'accepted',
        'ignoredOlder',
        'accepted',
        'error',
      ]);
      final updated = resources.get(
        'user-1',
        SyncedResourceType.exercise,
        'exercise-1',
      );
      expect(updated?.serverId, 'existing-server-id');
      expect(updated?.payloadJson, {'name': 'new'});
      expect(updated?.schemaVersion, 2);
      expect(updated?.originDeviceId, 'device-1');
    },
  );

  test(
    'pull returns resources newer than cursor including soft deletes',
    () async {
      resources
        ..put(
          _resource(
            serverId: 'resource-1',
            resourceType: SyncedResourceType.exercise,
            clientId: 'exercise-1',
            serverUpdatedAt: DateTime.utc(2026, 7, 19, 10),
          ),
        )
        ..put(
          _resource(
            serverId: 'resource-2',
            resourceType: SyncedResourceType.program,
            clientId: 'program-1',
            serverUpdatedAt: DateTime.utc(2026, 7, 19, 11),
            deletedAt: DateTime.utc(2026, 7, 19, 10, 30),
          ),
        );
      final useCase = PullSyncUseCase(resources: resources, clock: clock);

      final result = await useCase.execute(
        ownerUserId: 'user-1',
        since: DateTime.utc(2026, 7, 19, 10, 30),
      );

      expect(result.items.map((item) => item.clientId), ['program-1']);
      expect(result.items.single.deletedAt, DateTime.utc(2026, 7, 19, 10, 30));
      expect(result.serverCursor, DateTime.utc(2026, 7, 19, 11));
    },
  );

  test('exchange pushes first then pulls the updated state', () async {
    final push = PushSyncUseCase(resources: resources, clock: clock, ids: ids);
    final pull = PullSyncUseCase(resources: resources, clock: clock);
    final useCase = ExchangeSyncUseCase(push: push, pull: pull);

    final result = await useCase.execute(
      ownerUserId: 'user-1',
      deviceId: 'device-1',
      items: [
        PushSyncItemInput(
          resourceType: 'mediaAsset',
          clientId: 'media-1',
          schemaVersion: 1,
          clientUpdatedAt: DateTime.utc(2026, 7, 19, 10),
          deletedAt: null,
          payloadJson: {'localUri': 'file://image.jpg'},
        ),
      ],
    );

    expect(result.push.results.single.status, 'accepted');
    expect(result.pull.items.single.clientId, 'media-1');
  });
}

SyncedResource _resource({
  required String serverId,
  required SyncedResourceType resourceType,
  required String clientId,
  DateTime? clientUpdatedAt,
  DateTime? serverUpdatedAt,
  DateTime? deletedAt,
  Map<String, Object?> payloadJson = const {},
}) {
  return SyncedResource(
    serverId: serverId,
    ownerUserId: 'user-1',
    resourceType: resourceType,
    clientId: clientId,
    payloadJson: payloadJson,
    schemaVersion: 1,
    clientUpdatedAt: clientUpdatedAt ?? DateTime.utc(2026, 7, 19, 9),
    serverUpdatedAt: serverUpdatedAt ?? DateTime.utc(2026, 7, 19, 10),
    deletedAt: deletedAt,
    originDeviceId: 'device-1',
  );
}

final class _FakeSyncedResourceRepository implements SyncedResourceRepository {
  final _items = <String, SyncedResource>{};

  void put(SyncedResource resource) {
    _items[_key(
          resource.ownerUserId,
          resource.resourceType,
          resource.clientId,
        )] =
        resource;
  }

  SyncedResource? get(
    String ownerUserId,
    SyncedResourceType resourceType,
    String clientId,
  ) {
    return _items[_key(ownerUserId, resourceType, clientId)];
  }

  @override
  Future<SyncWriteResult> upsertWithLww(SyncedResource resource) async {
    final key = _key(
      resource.ownerUserId,
      resource.resourceType,
      resource.clientId,
    );
    final existing = _items[key];
    if (existing != null &&
        !resource.clientUpdatedAt.isAfter(existing.clientUpdatedAt)) {
      return SyncWriteResult(
        status: SyncWriteStatus.ignoredOlder,
        resource: existing,
      );
    }
    final written = existing == null
        ? resource
        : SyncedResource(
            serverId: existing.serverId,
            ownerUserId: existing.ownerUserId,
            resourceType: existing.resourceType,
            clientId: existing.clientId,
            payloadJson: resource.payloadJson,
            schemaVersion: resource.schemaVersion,
            clientUpdatedAt: resource.clientUpdatedAt,
            serverUpdatedAt: resource.serverUpdatedAt,
            deletedAt: resource.deletedAt,
            originDeviceId: resource.originDeviceId,
          );
    _items[key] = written;
    return SyncWriteResult(status: SyncWriteStatus.accepted, resource: written);
  }

  @override
  Future<List<SyncedResource>> findAllForUserSince({
    required String ownerUserId,
    DateTime? since,
  }) async {
    final result =
        _items.values
            .where((item) => item.ownerUserId == ownerUserId)
            .where(
              (item) =>
                  since == null || item.serverUpdatedAt.isAfter(since.toUtc()),
            )
            .toList()
          ..sort(
            (left, right) =>
                left.serverUpdatedAt.compareTo(right.serverUpdatedAt),
          );
    return result;
  }
}

String _key(
  String ownerUserId,
  SyncedResourceType resourceType,
  String clientId,
) {
  return '$ownerUserId:${resourceType.wireName}:$clientId';
}

final class _FakeClock implements Clock {
  _FakeClock(this.value);

  DateTime value;

  @override
  DateTime now() => value;
}

final class _FakeIds implements IdGenerator {
  var _next = 0;

  @override
  String newId() {
    _next += 1;
    return 'id-$_next';
  }
}
