import 'dart:convert';

import 'package:gametime_server/api/router.dart';
import 'package:gametime_server/api/sync_api.dart';
import 'package:gametime_server/application/application.dart';
import 'package:gametime_server/domain/domain.dart';
import 'package:shelf/shelf.dart';
import 'package:test/test.dart';

void main() {
  test('sync routes require bearer authentication', () async {
    final handler = buildApiHandler(syncApi: _syncApi());

    final response = await handler(
      Request('GET', Uri.parse('http://localhost/sync/pull')),
    );

    expect(response.statusCode, 401);
  });

  test('push parses item errors without blocking valid items', () async {
    final repository = _FakeSyncedResourceRepository();
    final handler = buildApiHandler(syncApi: _syncApi(resources: repository));

    final response = await handler(
      Request(
        'POST',
        Uri.parse('http://localhost/sync/push'),
        headers: {'authorization': 'Bearer valid-token'},
        body: jsonEncode({
          'deviceId': 'device-1',
          'items': [
            {
              'resourceType': 'exercise',
              'clientId': 'exercise-1',
              'schemaVersion': 1,
              'clientUpdatedAt': '2026-07-19T10:00:00Z',
              'deletedAt': null,
              'payload': {'name': 'Squat'},
            },
            {
              'resourceType': 'bad',
              'clientId': 'bad-1',
              'schemaVersion': 1,
              'clientUpdatedAt': '2026-07-19T10:00:00Z',
              'payload': {},
            },
            'malformed',
          ],
        }),
      ),
    );

    final body = jsonDecode(await response.readAsString()) as Map;
    final results = body['results'] as List;

    expect(response.statusCode, 200);
    expect(results.map((item) => (item as Map)['status']), [
      'accepted',
      'error',
      'error',
    ]);
    expect(repository.items.single.clientId, 'exercise-1');
  });
}

SyncApi _syncApi({_FakeSyncedResourceRepository? resources}) {
  final repository = resources ?? _FakeSyncedResourceRepository();
  final clock = _FakeClock(DateTime.utc(2026, 7, 19, 12));
  final ids = _FakeIds();
  final user = UserAccount(
    id: 'user-1',
    email: 'user@example.com',
    passwordHash: 'hash',
    createdAt: clock.now(),
    updatedAt: clock.now(),
  );
  final session = AuthSession(
    id: 'session-1',
    userId: user.id,
    tokenHash: 'token-hash:valid-token',
    issuedAt: clock.now(),
    expiresAt: clock.now().add(const Duration(days: 1)),
  );
  final users = _FakeUserRepository(user);
  final sessions = _FakeAuthSessionRepository(session);
  final tokens = _FakeTokenService();
  final authenticate = AuthenticateRequestUseCase(
    users: users,
    sessions: sessions,
    tokens: tokens,
    clock: clock,
  );
  final push = PushSyncUseCase(resources: repository, clock: clock, ids: ids);
  final pull = PullSyncUseCase(resources: repository, clock: clock);
  return SyncApi(
    pushSync: push,
    pullSync: pull,
    exchangeSync: ExchangeSyncUseCase(push: push, pull: pull),
    authenticateRequest: authenticate,
  );
}

final class _FakeSyncedResourceRepository implements SyncedResourceRepository {
  final items = <SyncedResource>[];

  @override
  Future<SyncWriteResult> upsertWithLww(SyncedResource resource) async {
    items.add(resource);
    return SyncWriteResult(
      status: SyncWriteStatus.accepted,
      resource: resource,
    );
  }

  @override
  Future<List<SyncedResource>> findAllForUserSince({
    required String ownerUserId,
    DateTime? since,
  }) async {
    return items
        .where((item) => item.ownerUserId == ownerUserId)
        .where((item) => since == null || item.serverUpdatedAt.isAfter(since))
        .toList();
  }
}

final class _FakeUserRepository implements UserRepository {
  const _FakeUserRepository(this.user);

  final UserAccount user;

  @override
  Future<UserAccount?> findByEmail(String email) async => user;

  @override
  Future<UserAccount?> findById(String id) async => id == user.id ? user : null;

  @override
  Future<void> insert(UserAccount user) async {}

  @override
  Future<void> updatePasswordHash({
    required String userId,
    required String passwordHash,
    required DateTime updatedAt,
  }) async {}
}

final class _FakeAuthSessionRepository implements AuthSessionRepository {
  const _FakeAuthSessionRepository(this.session);

  final AuthSession session;

  @override
  Future<AuthSession?> findByTokenHash(String tokenHash) async {
    return tokenHash == session.tokenHash ? session : null;
  }

  @override
  Future<void> insert(AuthSession session) async {}

  @override
  Future<void> revoke({
    required String sessionId,
    required DateTime revokedAt,
  }) async {}
}

final class _FakeTokenService implements OpaqueTokenService {
  @override
  String generateToken() => 'valid-token';

  @override
  String hashToken(String token) => 'token-hash:$token';
}

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 'server-id-$_next';
  }
}
