import 'dart:convert';

import 'package:gametime_server/api/auth_api.dart';
import 'package:gametime_server/api/router.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(
    'register endpoint returns the created user without password data',
    () async {
      final users = _FakeUserRepository();
      final handler = buildApiHandler(authApi: _authApi(users: users));

      final response = await handler(
        Request(
          'POST',
          Uri.parse('http://localhost/auth/register'),
          body: jsonEncode({
            'email': ' USER@example.com ',
            'password': 'password123',
            'displayName': ' User ',
          }),
        ),
      );

      final body = jsonDecode(await response.readAsString()) as Map;

      expect(response.statusCode, 201);
      expect(body, {'userId': 'id-1', 'email': 'user@example.com'});
      expect(body.containsKey('password'), isFalse);
      expect(body.containsKey('passwordHash'), isFalse);
    },
  );

  test('register endpoint returns 409 when email is already used', () async {
    final users = _FakeUserRepository();
    await users.insert(
      UserAccount(
        id: 'existing-user',
        email: 'user@example.com',
        passwordHash: 'hash',
        createdAt: DateTime.utc(2026, 7, 19),
        updatedAt: DateTime.utc(2026, 7, 19),
      ),
    );
    final handler = buildApiHandler(authApi: _authApi(users: users));

    final response = await handler(
      Request(
        'POST',
        Uri.parse('http://localhost/auth/register'),
        body: jsonEncode({
          'email': 'user@example.com',
          'password': 'password123',
        }),
      ),
    );

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

  test('login endpoint returns a token and expiration', () async {
    final users = _FakeUserRepository();
    await users.insert(
      UserAccount(
        id: 'user-1',
        email: 'user@example.com',
        passwordHash: 'hashed:password123',
        createdAt: DateTime.utc(2026, 7, 19),
        updatedAt: DateTime.utc(2026, 7, 19),
      ),
    );
    final handler = buildApiHandler(authApi: _authApi(users: users));

    final response = await handler(
      Request(
        'POST',
        Uri.parse('http://localhost/auth/login'),
        body: jsonEncode({
          'email': 'user@example.com',
          'password': 'password123',
          'deviceLabel': 'phone',
        }),
      ),
    );

    final body = jsonDecode(await response.readAsString()) as Map;

    expect(response.statusCode, 200);
    expect(body['token'], 'clear-token');
    expect(body['expiresAt'], '2026-08-18T12:00:00.000Z');
  });

  test('login endpoint returns 401 for invalid credentials', () async {
    final users = _FakeUserRepository();
    await users.insert(
      UserAccount(
        id: 'user-1',
        email: 'user@example.com',
        passwordHash: 'hashed:password123',
        createdAt: DateTime.utc(2026, 7, 19),
        updatedAt: DateTime.utc(2026, 7, 19),
      ),
    );
    final handler = buildApiHandler(authApi: _authApi(users: users));

    final response = await handler(
      Request(
        'POST',
        Uri.parse('http://localhost/auth/login'),
        body: jsonEncode({
          'email': 'user@example.com',
          'password': 'wrong-password',
        }),
      ),
    );

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

  test('logout endpoint revokes the bearer session', () async {
    final users = _FakeUserRepository();
    await users.insert(
      UserAccount(
        id: 'user-1',
        email: 'user@example.com',
        passwordHash: 'hashed:password123',
        createdAt: DateTime.utc(2026, 7, 19),
        updatedAt: DateTime.utc(2026, 7, 19),
      ),
    );
    final sessions = _FakeAuthSessionRepository();
    await sessions.insert(
      AuthSession(
        id: 'session-1',
        userId: 'user-1',
        tokenHash: 'token-hash:clear-token',
        issuedAt: DateTime.utc(2026, 7, 19, 12),
        expiresAt: DateTime.utc(2026, 7, 20, 12),
      ),
    );
    final handler = buildApiHandler(
      authApi: _authApi(users: users, sessions: sessions),
    );

    final response = await handler(
      Request(
        'POST',
        Uri.parse('http://localhost/auth/logout'),
        headers: {'authorization': 'Bearer clear-token'},
      ),
    );

    expect(response.statusCode, 204);
    expect(sessions.revokedSessionIds, ['session-1']);
  });
}

AuthApi _authApi({
  _FakeUserRepository? users,
  _FakeAuthSessionRepository? sessions,
}) {
  final userRepository = users ?? _FakeUserRepository();
  final sessionRepository = sessions ?? _FakeAuthSessionRepository();
  final passwordHasher = _FakePasswordHasher();
  final tokens = _FakeTokenService();
  final clock = _FakeClock(DateTime.utc(2026, 7, 19, 12));
  final ids = _FakeIds();
  final authenticateRequest = AuthenticateRequestUseCase(
    users: userRepository,
    sessions: sessionRepository,
    tokens: tokens,
    clock: clock,
  );
  return AuthApi(
    registerUser: RegisterUserUseCase(
      users: userRepository,
      passwordHasher: passwordHasher,
      clock: clock,
      ids: ids,
    ),
    login: LoginUseCase(
      users: userRepository,
      sessions: sessionRepository,
      passwordHasher: passwordHasher,
      tokens: tokens,
      clock: clock,
      ids: ids,
    ),
    logout: LogoutUseCase(
      sessions: sessionRepository,
      tokens: tokens,
      clock: clock,
    ),
    authenticateRequest: authenticateRequest,
  );
}

final class _FakeUserRepository implements UserRepository {
  final byId = <String, UserAccount>{};
  final byEmail = <String, UserAccount>{};

  @override
  Future<UserAccount?> findByEmail(String email) async {
    return byEmail[email.trim().toLowerCase()];
  }

  @override
  Future<UserAccount?> findById(String id) async => byId[id];

  @override
  Future<void> insert(UserAccount user) async {
    byId[user.id] = user;
    byEmail[user.email] = user;
  }

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

final class _FakeAuthSessionRepository implements AuthSessionRepository {
  final byTokenHash = <String, AuthSession>{};
  final revokedSessionIds = <String>[];

  @override
  Future<void> insert(AuthSession session) async {
    byTokenHash[session.tokenHash] = session;
  }

  @override
  Future<AuthSession?> findByTokenHash(String tokenHash) async {
    return byTokenHash[tokenHash];
  }

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

final class _FakePasswordHasher implements PasswordHasher {
  @override
  Future<String> hash(String password) async => 'hashed:$password';

  @override
  Future<bool> verify({
    required String password,
    required String passwordHash,
  }) async {
    return passwordHash == 'hashed:$password';
  }
}

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

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

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

  final DateTime value;

  @override
  DateTime now() => value;
}

final class _FakeIds implements IdGenerator {
  var _next = 0;

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