import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'package:tdc114plus/src/app.dart';
import 'package:tdc114plus/src/core/actions/contact_launcher.dart';
import 'package:tdc114plus/src/core/network/api_error.dart';
import 'package:tdc114plus/src/features/auth/data/auth_repository.dart';
import 'package:tdc114plus/src/features/auth/data/auth_session_store.dart';
import 'package:tdc114plus/src/features/auth/domain/auth_models.dart';
import 'package:tdc114plus/src/features/directory/data/directory_repository.dart';
import 'package:tdc114plus/src/features/directory/data/mock_directory_data.dart';
import 'package:tdc114plus/src/features/directory/domain/employee.dart';
import 'package:tdc114plus/src/features/favorites/data/favorites_repository.dart';
import 'package:tdc114plus/src/features/favorites/domain/favorite_employee.dart';
import 'package:tdc114plus/src/features/organization/data/organization_api_client.dart';
import 'package:tdc114plus/src/features/organization/domain/organization_models.dart';

void main() {
  testWidgets('shows link-based login screen', (WidgetTester tester) async {
    await _pumpApp(tester);
    await tester.pumpAndSettle();

    expect(find.text('로그인 링크 발송'), findsOneWidget);
    expect(find.byType(TextField), findsOneWidget);
    expect(find.text('로그인 링크 보내기'), findsOneWidget);
  });

  testWidgets('restores saved session on app launch', (
    WidgetTester tester,
  ) async {
    await _pumpApp(
      tester,
      repository: _FakeAuthRepository(hasStoredSession: true),
    );
    await tester.pumpAndSettle();

    expect(find.text('직원검색'), findsWidgets);
    expect(find.text('김하늘'), findsOneWidget);
    expect(find.text('최민준'), findsOneWidget);
    expect(find.text('박서준'), findsNothing);
    expect(find.text('이도윤'), findsNothing);
    expect(find.text('검색 결과 2명'), findsOneWidget);
  });

  testWidgets('shows link login guidance with phone input', (
    WidgetTester tester,
  ) async {
    await _pumpApp(tester);

    expect(find.byType(TextField), findsOneWidget);
    expect(
      find.textContaining('입력하신 전화번호로 바론SSO 로그인 링크가 발송됩니다.'),
      findsNothing,
    );
    expect(find.textContaining('문자 승인 후 앱으로 돌아오면 자동 로그인됩니다.'), findsOneWidget);
  });

  testWidgets('keeps login link button visible when keyboard is open', (
    WidgetTester tester,
  ) async {
    tester.view.physicalSize = const Size(750, 1334);
    tester.view.devicePixelRatio = 1;
    addTearDown(tester.view.resetPhysicalSize);
    addTearDown(tester.view.resetDevicePixelRatio);

    await _pumpApp(tester);

    await tester.tap(find.byType(TextField));
    tester.view.viewInsets = const FakeViewPadding(bottom: 420);
    addTearDown(tester.view.resetViewInsets);
    await tester.pumpAndSettle();

    expect(find.text('로그인 링크 보내기'), findsOneWidget);
    expect(tester.getBottomLeft(find.text('로그인 링크 보내기')).dy, lessThan(914));
  });

  testWidgets('requests login link and shows pending status', (
    WidgetTester tester,
  ) async {
    await _pumpApp(
      tester,
      repository: _FakeAuthRepository(pollMode: _FakePollMode.pendingOnly),
    );

    await tester.enterText(find.byType(TextField), '010-1234-5678');
    await tester.tap(find.text('로그인 링크 보내기'));
    await tester.pump();

    expect(
      find.textContaining('문자 링크를 누른 뒤 TDC114PLUS 앱으로 돌아오세요.'),
      findsOneWidget,
    );
    expect(find.text('승인 대기 중'), findsOneWidget);
    expect(find.text('다른 번호 입력'), findsOneWidget);
  });

  testWidgets('restores pending login link and completes polling', (
    WidgetTester tester,
  ) async {
    final repository = _FakeAuthRepository(
      initialPendingLink: const PhoneLoginLinkInitResponse(
        status: 'pending',
        pendingRef: 'restored-pending-ref',
        expiresIn: 180,
        interval: 1,
        resendAfter: 0,
      ),
    );
    await _pumpApp(tester, repository: repository);
    await tester.pumpAndSettle();

    expect(repository.polledRefs, contains('restored-pending-ref'));
    expect(find.text('직원검색'), findsWidgets);
  });

  testWidgets('defaults directory scope to logged-in user tenant', (
    WidgetTester tester,
  ) async {
    await _openDirectory(tester);

    expect(find.text('김하늘'), findsOneWidget);
    expect(find.text('최민준'), findsOneWidget);
    expect(find.text('박서준'), findsNothing);
    expect(find.text('이도윤'), findsNothing);
    expect(find.text('정수빈'), findsNothing);
  });

  testWidgets('keeps initial selection at logged-in pinned team scope', (
    WidgetTester tester,
  ) async {
    await _pumpApp(
      tester,
      repository: _FakeAuthRepository(hasStoredSession: true),
      directoryRepository: const _OrganizationScopedDirectoryRepository(),
    );
    await tester.pumpAndSettle();

    expect(find.text('Hanmac'), findsNothing);
    expect(find.text('김하늘'), findsOneWidget);
    expect(find.text('최민준'), findsOneWidget);
    expect(find.text('박서준'), findsNothing);
    expect(find.text('검색 결과 2명'), findsOneWidget);
  });

  testWidgets('filters employees by search query', (WidgetTester tester) async {
    await _openDirectory(tester);

    await tester.enterText(find.byType(TextField), '서준');
    await tester.pumpAndSettle();

    expect(find.text('박서준'), findsNothing);
    expect(find.text('김하늘'), findsNothing);
    expect(find.text('검색 결과 0명'), findsOneWidget);
  });

  testWidgets('allows one hangul character for employee search', (
    WidgetTester tester,
  ) async {
    await _openDirectory(tester);

    await tester.enterText(find.byType(TextField), '김');
    await tester.pumpAndSettle();

    expect(find.text('김하늘'), findsOneWidget);
    expect(find.text('박서준'), findsNothing);
  });

  testWidgets('searches employees inside selected company subtree', (
    WidgetTester tester,
  ) async {
    await _openDirectory(tester);

    await tester.tap(find.text('전체'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('조직도'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('한맥').first);
    await tester.pumpAndSettle();
    await tester.enterText(find.byType(TextField), '서준');
    await tester.pumpAndSettle();

    expect(find.text('박서준'), findsOneWidget);
    expect(find.text('김하늘'), findsNothing);
    expect(find.text('검색 결과 1명'), findsOneWidget);
  });

  testWidgets('applies local name filter even when API returns broad results', (
    WidgetTester tester,
  ) async {
    await _pumpApp(
      tester,
      repository: _FakeAuthRepository(
        hasStoredSession: true,
        department: '통합시스템',
      ),
      directoryRepository: const _BroadSearchDirectoryRepository(),
    );
    await tester.pumpAndSettle();

    await tester.enterText(find.byType(TextField), '한');
    await tester.pumpAndSettle();

    expect(find.text('한승민'), findsOneWidget);
    expect(find.text('김윤재'), findsNothing);
    expect(find.text('권오재'), findsNothing);
  });

  testWidgets('keeps current scope list when phone query is under minimum', (
    WidgetTester tester,
  ) async {
    await _openDirectory(tester);

    await tester.enterText(find.byType(TextField), '010');
    await tester.pumpAndSettle();

    expect(find.text('전화번호는 010 제외 2자리 이상 입력'), findsOneWidget);
    expect(find.text('김하늘'), findsOneWidget);
    expect(find.text('박서준'), findsNothing);
    expect(find.text('최민준'), findsOneWidget);
    expect(find.text('검색 결과 2명'), findsOneWidget);
  });

  testWidgets('searches only within selected tenant scope', (
    WidgetTester tester,
  ) async {
    await _openDirectory(tester);

    await tester.tap(find.text('TDC'));
    await tester.pumpAndSettle();
    await tester.enterText(find.byType(TextField), '김');
    await tester.pumpAndSettle();

    expect(find.text('김하늘'), findsNothing);
    expect(find.text('검색 결과 0명'), findsOneWidget);
  });

  testWidgets('filters employees by tenant chip', (WidgetTester tester) async {
    await _openDirectory(tester);

    await tester.tap(find.text('TDC'));
    await tester.pumpAndSettle();

    expect(find.text('이도윤'), findsOneWidget);
    expect(find.text('정수빈'), findsOneWidget);
    expect(find.text('김하늘'), findsNothing);
    expect(find.text('검색 결과 2명'), findsOneWidget);
  });

  testWidgets('starts hierarchy drilldown from all chip', (
    WidgetTester tester,
  ) async {
    await _openDirectory(tester);

    await tester.tap(find.text('전체'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('조직도'));
    await tester.pumpAndSettle();

    expect(find.text('가족사 2개'), findsOneWidget);
    expect(find.text('한맥'), findsWidgets);
    expect(find.text('TDC'), findsWidgets);
    expect(find.text('김하늘'), findsNothing);
  });

  testWidgets('does not load employees while showing child organizations', (
    WidgetTester tester,
  ) async {
    final directoryRepository = _CountingDirectoryRepository();
    await _pumpApp(
      tester,
      repository: _FakeAuthRepository(hasStoredSession: true),
      directoryRepository: directoryRepository,
    );
    await tester.pumpAndSettle();

    await tester.tap(find.text('전체'));
    await tester.pumpAndSettle();
    final callsBeforeHierarchy = directoryRepository.calls;
    await tester.tap(find.text('조직도'));
    await tester.pumpAndSettle();

    expect(find.text('가족사 2개'), findsOneWidget);
    expect(directoryRepository.calls, callsBeforeHierarchy);
  });

  testWidgets(
    'keeps pinned team chip visible after selecting another company',
    (WidgetTester tester) async {
      await _openDirectory(tester);

      expect(find.widgetWithText(FilterChip, '기술연구소'), findsWidgets);

      await tester.tap(find.text('TDC'));
      await tester.pumpAndSettle();

      expect(find.widgetWithText(FilterChip, '기술연구소'), findsWidgets);
      expect(find.text('이도윤'), findsOneWidget);
      expect(find.text('정수빈'), findsOneWidget);
    },
  );

  testWidgets('shows pinned team chip before all chip', (
    WidgetTester tester,
  ) async {
    await _openDirectory(tester);

    final chips = tester
        .widgetList<FilterChip>(find.byType(FilterChip))
        .toList();
    final labels = chips
        .map((chip) => (chip.label as Text).data ?? '')
        .where((label) => label.isNotEmpty)
        .toList();

    final pinnedIndex = labels.indexOf('기술연구소');
    final allIndex = labels.indexOf('전체');

    expect(pinnedIndex, isNonNegative);
    expect(allIndex, isNonNegative);
    expect(pinnedIndex, lessThan(allIndex));
  });

  testWidgets('shows horizontal scrollbar for tenant chips', (
    WidgetTester tester,
  ) async {
    await _openDirectory(tester);

    expect(find.byType(Scrollbar), findsOneWidget);
  });

  testWidgets('uses emphasized style for selected tenant chip', (
    WidgetTester tester,
  ) async {
    await _openDirectory(tester);

    final chip = tester.widget<FilterChip>(
      find.widgetWithText(FilterChip, '기술연구소').first,
    );

    expect(chip.selected, isTrue);
    expect(chip.showCheckmark, isFalse);
    expect(chip.selectedColor, isNotNull);
  });

  testWidgets('selects pinned team chip on initial entry when available', (
    WidgetTester tester,
  ) async {
    await _openDirectory(tester);

    final pinnedChip = tester.widget<FilterChip>(
      find.widgetWithText(FilterChip, '기술연구소').first,
    );
    final companyChip = tester.widget<FilterChip>(
      find.widgetWithText(FilterChip, '한맥').first,
    );

    expect(pinnedChip.selected, isTrue);
    expect(companyChip.selected, isFalse);
    expect(find.text('김하늘'), findsOneWidget);
    expect(find.text('최민준'), findsOneWidget);
    expect(find.text('박서준'), findsNothing);
    expect(find.text('검색 결과 2명'), findsOneWidget);
  });

  testWidgets(
    'resolves initial team from org chart when session scope is empty',
    (WidgetTester tester) async {
      await _pumpApp(
        tester,
        repository: _FakeAuthRepository(
          hasStoredSession: true,
          tenantSlug: '',
          department: '',
        ),
      );
      await tester.pumpAndSettle();

      final pinnedChip = tester.widget<FilterChip>(
        find.widgetWithText(FilterChip, '기술연구소').first,
      );
      final allChip = tester.widget<FilterChip>(
        find.widgetWithText(FilterChip, '전체').first,
      );

      expect(pinnedChip.selected, isTrue);
      expect(allChip.selected, isFalse);
      expect(find.text('김하늘'), findsOneWidget);
      expect(find.text('최민준'), findsOneWidget);
      expect(find.text('박서준'), findsNothing);
      expect(find.text('검색 결과 2명'), findsOneWidget);
    },
  );

  testWidgets(
    'shows synthetic pinned team chip before all chip when tenant node is missing',
    (WidgetTester tester) async {
      await _pumpApp(
        tester,
        repository: _FakeAuthRepository(
          hasStoredSession: true,
          department: 'IS3',
        ),
      );
      await tester.pumpAndSettle();

      final chips = tester
          .widgetList<FilterChip>(find.byType(FilterChip))
          .toList();
      final labels = chips
          .map((chip) => (chip.label as Text).data ?? '')
          .where((label) => label.isNotEmpty)
          .toList();

      final pinnedIndex = labels.indexOf('IS3');
      final allIndex = labels.indexOf('전체');

      expect(pinnedIndex, isNonNegative);
      expect(allIndex, isNonNegative);
      expect(pinnedIndex, lessThan(allIndex));
    },
  );

  testWidgets(
    'does not duplicate affiliation text when tenant and department match',
    (WidgetTester tester) async {
      await _pumpApp(
        tester,
        repository: _FakeAuthRepository(
          hasStoredSession: true,
          department: 'IS3',
        ),
        directoryRepository: const _DuplicateAffiliationDirectoryRepository(),
      );
      await tester.pumpAndSettle();

      expect(find.text('IS3 · +821012345678'), findsOneWidget);
      expect(find.text('IS3 · IS3 · +821012345678'), findsNothing);
    },
  );

  testWidgets('drills down from all to company to department to employees', (
    WidgetTester tester,
  ) async {
    await _openDirectory(tester);

    await tester.tap(find.text('전체'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('조직도'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('한맥').first);
    await tester.pumpAndSettle();

    expect(find.text('ICT사업부'), findsWidgets);
    expect(find.text('기술연구소'), findsWidgets);

    await tester.tap(find.text('기술연구소').last);
    await tester.pumpAndSettle();

    expect(find.text('김하늘'), findsOneWidget);
    expect(find.text('최민준'), findsOneWidget);
    expect(find.text('박서준'), findsNothing);
  });

  testWidgets('shows direct employees for non-leaf organization separately', (
    WidgetTester tester,
  ) async {
    await _pumpApp(
      tester,
      repository: _FakeAuthRepository(hasStoredSession: true),
      directoryRepository: const _DirectAffiliationDirectoryRepository(),
    );
    await tester.pumpAndSettle();

    await tester.tap(find.text('전체'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('조직도'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('한맥').first);
    await tester.pumpAndSettle();

    expect(find.text('한맥 조직 관리자'), findsOneWidget);
    expect(find.text('이영희'), findsOneWidget);
    expect(find.text('김하늘'), findsNothing);
    expect(
      tester.getTopLeft(find.text('한맥 조직 관리자')).dy,
      lessThan(tester.getTopLeft(find.text('ICT사업부').last).dy),
    );
  });

  testWidgets(
    'excludes root employees that also belong to child organizations from direct employees',
    (WidgetTester tester) async {
      await _pumpApp(
        tester,
        repository: _FakeAuthRepository(hasStoredSession: true),
        directoryRepository: const _OverlappingMembershipDirectoryRepository(),
      );
      await tester.pumpAndSettle();

      await tester.tap(find.text('전체'));
      await tester.pumpAndSettle();
      await tester.tap(find.text('조직도'));
      await tester.pumpAndSettle();
      await tester.tap(find.text('한맥').first);
      await tester.pumpAndSettle();

      expect(find.text('한맥 조직 관리자'), findsOneWidget);
      expect(find.text('이영희'), findsOneWidget);
      expect(find.text('김하늘'), findsNothing);
      expect(find.text('박서준'), findsNothing);
    },
  );

  testWidgets('shows divider between top chips and breadcrumb chips', (
    WidgetTester tester,
  ) async {
    await _openDirectory(tester);

    await tester.tap(find.text('전체'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('조직도'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('한맥').first);
    await tester.pumpAndSettle();

    expect(find.byType(Divider), findsAtLeastNWidgets(1));
  });

  testWidgets('shows emphasized icon on selected breadcrumb chip', (
    WidgetTester tester,
  ) async {
    await _openDirectory(tester);

    await tester.tap(find.text('전체'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('조직도'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('한맥').first);
    await tester.pumpAndSettle();
    await tester.tap(find.text('기술연구소').last);
    await tester.pumpAndSettle();

    expect(find.byIcon(Icons.check_circle), findsOneWidget);
  });

  testWidgets('sorts organization employees by manager then grade then name', (
    WidgetTester tester,
  ) async {
    await _pumpApp(
      tester,
      repository: _FakeAuthRepository(hasStoredSession: true),
      directoryRepository: const _OrganizationSortingDirectoryRepository(),
    );
    await tester.pumpAndSettle();

    await tester.tap(find.text('기술연구소').last);
    await tester.pumpAndSettle();
    await tester.tap(find.text('조직도'));
    await tester.pumpAndSettle();

    expect(find.text('인프라 BIM3'), findsOneWidget);
    expect(find.text('미지정'), findsOneWidget);

    final leaderY = tester.getTopLeft(find.text('김지영')).dy;
    final directorY = tester.getTopLeft(find.text('박도현')).dy;
    final principalY = tester.getTopLeft(find.text('김성엽')).dy;
    final seniorY = tester.getTopLeft(find.text('한반석')).dy;
    final staffY = tester.getTopLeft(find.text('유서진')).dy;

    expect(leaderY, lessThan(principalY));
    expect(principalY, lessThan(directorY));
    expect(directorY, lessThan(seniorY));
    expect(seniorY, lessThan(staffY));
  });

  testWidgets('returns to employee list after switching organization then employees', (
    WidgetTester tester,
  ) async {
    await _openDirectory(tester);

    await tester.tap(find.text('전체'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('조직도'));
    await tester.pumpAndSettle();
    expect(find.text('가족사 2개'), findsOneWidget);
    expect(find.text('한맥'), findsWidgets);

    await tester.tap(find.text('직원'));
    await tester.pumpAndSettle();

    expect(find.text('김하늘'), findsOneWidget);
    expect(find.byType(Card), findsWidgets);
  });

  testWidgets('shows favorite employees view', (WidgetTester tester) async {
    await _openDirectory(tester);

    await tester.tap(find.text('즐겨찾기'));
    await tester.pumpAndSettle();

    expect(find.text('김하늘'), findsOneWidget);
    expect(find.text('박서준'), findsNothing);
    expect(find.text('검색 결과 1명'), findsOneWidget);
  });

  testWidgets('toggles favorite employees', (WidgetTester tester) async {
    await _openDirectory(tester);

    await tester.tap(find.byTooltip('즐겨찾기 추가').first);
    await tester.pumpAndSettle();
    await tester.tap(find.text('즐겨찾기'));
    await tester.pumpAndSettle();

    expect(find.text('김하늘'), findsOneWidget);
    expect(find.text('최민준'), findsOneWidget);
    expect(find.text('검색 결과 2명'), findsOneWidget);
  });

  testWidgets('opens employee detail bottom sheet', (
    WidgetTester tester,
  ) async {
    await _openDirectory(tester);

    await tester.tap(find.text('김하늘'));
    await tester.pumpAndSettle();

    expect(find.text('haneul.kim@example.com'), findsOneWidget);
    expect(find.text('전화'), findsOneWidget);
    expect(find.text('문자'), findsOneWidget);
  });

  testWidgets('loads enriched employee detail in bottom sheet', (
    WidgetTester tester,
  ) async {
    await _pumpApp(
      tester,
      repository: _FakeAuthRepository(hasStoredSession: true),
      employeeDetailRepository: const _EnrichedEmployeeDetailRepository(),
    );

    await tester.tap(find.text('김하늘'));
    await tester.pump();
    await tester.pumpAndSettle();

    expect(find.text('kim.haneul@corp.example.com'), findsOneWidget);
    expect(find.text('보안 아키텍트'), findsOneWidget);
    expect(find.text('한맥, 기술연구소'), findsOneWidget);
  });

  testWidgets('falls back to list data when employee detail load fails', (
    WidgetTester tester,
  ) async {
    await _pumpApp(
      tester,
      repository: _FakeAuthRepository(hasStoredSession: true),
      employeeDetailRepository: const _FailingEmployeeDetailRepository(),
    );

    await tester.tap(find.text('김하늘'));
    await tester.pump();
    await tester.pumpAndSettle();

    expect(find.text('haneul.kim@example.com'), findsOneWidget);
    expect(find.text('Flutter 개발'), findsOneWidget);
  });

  testWidgets('disables contact actions when employee has no phone number', (
    WidgetTester tester,
  ) async {
    await _openDirectory(tester);

    await tester.enterText(find.byType(TextField), '최민준');
    await tester.pumpAndSettle();
    await tester.tap(find.text('최민준').last);
    await tester.pumpAndSettle();

    expect(
      tester
          .widget<FilledButton>(find.widgetWithText(FilledButton, '전화'))
          .onPressed,
      isNull,
    );
    expect(
      tester
          .widget<OutlinedButton>(find.widgetWithText(OutlinedButton, '문자'))
          .onPressed,
      isNull,
    );
  });

  testWidgets('honors employee detail action permissions', (
    WidgetTester tester,
  ) async {
    await _pumpApp(
      tester,
      repository: _FakeAuthRepository(hasStoredSession: true),
      employeeDetailRepository: const _ActionRestrictedEmployeeDetailRepository(),
    );

    await tester.tap(find.text('김하늘'));
    await tester.pump();
    await tester.pumpAndSettle();

    expect(
      tester
          .widget<FilledButton>(find.widgetWithText(FilledButton, '전화'))
          .onPressed,
      isNull,
    );
    expect(
      tester
          .widget<OutlinedButton>(find.widgetWithText(OutlinedButton, '문자'))
          .onPressed,
      isNotNull,
    );
  });

  testWidgets('toggles favorite from employee detail bottom sheet', (
    WidgetTester tester,
  ) async {
    await _openDirectory(tester);

    await tester.enterText(find.byType(TextField), '최민준');
    await tester.pumpAndSettle();
    await tester.tap(find.text('최민준').last);
    await tester.pumpAndSettle();
    await tester.tap(find.byTooltip('즐겨찾기 추가').last);
    await tester.pumpAndSettle();

    await tester.tap(find.text('즐겨찾기'));
    await tester.pumpAndSettle();

    expect(find.text('최민준').last, findsOneWidget);
  });

  testWidgets('routes call and sms actions through contact launcher', (
    WidgetTester tester,
  ) async {
    final launcher = _FakeContactLauncher();
    await _pumpApp(
      tester,
      repository: _FakeAuthRepository(hasStoredSession: true),
      launcher: launcher,
    );
    await tester.pumpAndSettle();

    await tester.tap(find.text('김하늘'));
    await tester.pumpAndSettle();

    await tester.tap(find.text('전화'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('문자'));
    await tester.pumpAndSettle();

    expect(launcher.calledNumbers, ['call:+821012345678', 'sms:+821012345678']);
  });

  testWidgets('returns to login when directory API requires relogin', (
    WidgetTester tester,
  ) async {
    final repository = _FakeAuthRepository(hasStoredSession: true);
    await _pumpApp(
      tester,
      repository: repository,
      directoryRepository: const _UnauthorizedDirectoryRepository(),
    );
    await tester.pumpAndSettle();

    expect(find.text('다시 로그인'), findsOneWidget);

    await tester.tap(find.text('다시 로그인'));
    await tester.pumpAndSettle();

    expect(repository.logoutCalled, isTrue);
    expect(find.text('로그인 링크 발송'), findsOneWidget);
  });

  testWidgets('logs out from directory app bar', (WidgetTester tester) async {
    final repository = _FakeAuthRepository(hasStoredSession: true);
    await _pumpApp(tester, repository: repository);
    await tester.pumpAndSettle();

    await tester.tap(find.byTooltip('로그아웃'));
    await tester.pumpAndSettle();

    expect(repository.logoutCalled, isTrue);
    expect(find.text('로그인 링크 발송'), findsOneWidget);
  });
}

Future<void> _openDirectory(WidgetTester tester) async {
  await _pumpApp(
    tester,
    repository: _FakeAuthRepository(hasStoredSession: true),
  );
  await tester.pumpAndSettle();

  if (find.text('직원검색').evaluate().isNotEmpty) {
    return;
  }
}

Future<void> _pumpApp(
  WidgetTester tester, {
  AuthRepository? repository,
  DirectoryRepository? directoryRepository,
  EmployeeDetailRepository? employeeDetailRepository,
  OrganizationRepository? organizationRepository,
  ContactLauncher? launcher,
}) async {
  await tester.pumpWidget(
    ProviderScope(
      overrides: [
        authRepositoryProvider.overrideWithValue(
          repository ?? _FakeAuthRepository(),
        ),
        directoryRepositoryProvider.overrideWithValue(
          directoryRepository ?? const _FakeDirectoryRepository(),
        ),
        employeeDetailRepositoryProvider.overrideWithValue(
          employeeDetailRepository ?? const _FakeEmployeeDetailRepository(),
        ),
        organizationRepositoryProvider.overrideWithValue(
          organizationRepository ?? const _FakeOrganizationRepository(),
        ),
        favoritesRepositoryProvider.overrideWithValue(
          _FakeFavoritesRepository(),
        ),
        contactLauncherProvider.overrideWithValue(
          launcher ?? _FakeContactLauncher(),
        ),
      ],
      child: const Tdc114PlusApp(),
    ),
  );
  await tester.pumpAndSettle();
}

class _FakeAuthRepository implements AuthRepository {
  _FakeAuthRepository({
    this.hasStoredSession = false,
    this.tenantSlug = 'hanmac',
    this.department = '기술연구소',
    this.pollMode = _FakePollMode.completeImmediately,
    this.initialPendingLink,
  });

  final bool hasStoredSession;
  final String tenantSlug;
  final String department;
  final _FakePollMode pollMode;
  final PhoneLoginLinkInitResponse? initialPendingLink;
  StoredAuthSession? _storedSession;
  PhoneLoginLinkInitResponse? _pendingLink;
  var _pendingCleared = false;
  final List<String> polledRefs = [];
  var logoutCalled = false;

  Future<PhoneLoginResponse> phoneLogin(String phoneNumber) async {
    final response = PhoneLoginResponse(
      status: 'ok',
      token: 'session-token',
      expiresAt: DateTime.parse('2026-07-02T12:00:00Z'),
      user: LoginUser(
        id: 'user-uuid',
        name: 'User One',
        phoneNumber: '+821012345678',
        tenantId: 'tenant-uuid',
        tenantName: 'Hanmac',
        tenantSlug: tenantSlug,
        department: department,
      ),
    );
    _storedSession = StoredAuthSession(
      token: response.token,
      expiresAt: response.expiresAt,
      user: response.user,
    );
    return response;
  }

  @override
  Future<PhoneLoginLinkInitResponse> requestPhoneLoginLink(
    String phoneNumber,
  ) async {
    _pendingCleared = false;
    _pendingLink = const PhoneLoginLinkInitResponse(
      status: 'pending',
      pendingRef: 'pending-ref',
      expiresIn: 180,
      interval: 1,
      resendAfter: 30,
    );
    return _pendingLink!;
  }

  @override
  Future<PhoneLoginLinkPollResponse> pollPhoneLoginLink(
    String pendingRef,
  ) async {
    polledRefs.add(pendingRef);
    if (pollMode == _FakePollMode.pendingOnly) {
      return const PhoneLoginLinkPollResponse(
        status: 'pending',
        code: 'authorization_pending',
        interval: 1,
      );
    }
    return PhoneLoginLinkPollResponse(
      status: 'ok',
      session: await phoneLogin('01012345678'),
    );
  }

  @override
  Future<PhoneLoginLinkInitResponse?> loadPendingPhoneLoginLink() async {
    if (_pendingCleared) {
      return null;
    }
    return _pendingLink ?? initialPendingLink;
  }

  @override
  Future<void> clearPendingPhoneLoginLink() async {
    _pendingLink = null;
    _pendingCleared = true;
  }

  @override
  Future<StoredAuthSession?> loadSession() async {
    if (_storedSession != null) {
      return _storedSession;
    }
    if (!hasStoredSession) {
      return null;
    }
    return StoredAuthSession(
      token: 'session-token',
      expiresAt: DateTime.now().toUtc().add(const Duration(hours: 1)),
      user: LoginUser(
        id: 'user-uuid',
        name: 'User One',
        phoneNumber: '+821012345678',
        tenantId: 'tenant-uuid',
        tenantName: 'Hanmac',
        tenantSlug: tenantSlug,
        department: department,
      ),
    );
  }

  @override
  Future<void> logout() async {
    logoutCalled = true;
    _pendingLink = null;
    _pendingCleared = true;
  }
}

enum _FakePollMode { completeImmediately, pendingOnly }

class _FakeDirectoryRepository implements DirectoryRepository {
  const _FakeDirectoryRepository();

  @override
  Future<List<Employee>> loadEmployees(DirectoryQuery query) async {
    return _filteredEmployees(query);
  }

  List<Employee> _filteredEmployees(DirectoryQuery query) {
    final normalizedQuery = query.query.replaceAll('-', '').toLowerCase();
    final selectedTenant = _findTenantBySlug(query.tenantSlug);
    final tenantSlugs = query.apiTenantSlugs;
    return mockEmployees.where((employee) {
      final matchesTenant =
          tenantSlugs == null ||
          tenantSlugs.contains(employee.tenantSlug) ||
          (selectedTenant != null &&
              selectedTenant.type != 'COMPANY' &&
              (employee.department ?? '') == selectedTenant.name);
      final matchesDepartment =
          query.department == null || employee.department == query.department;
      final phone = employee.phoneNumber.replaceAll(RegExp(r'[^0-9]'), '');
      final phoneDisplay = employee.phoneDisplay?.replaceAll('-', '') ?? '';
      final searchable = [
        employee.name,
        employee.phoneNumber,
        employee.phoneDisplay ?? '',
        employee.email ?? '',
        employee.tenantName,
        employee.department ?? '',
        employee.grade ?? '',
        employee.position ?? '',
        employee.jobTitle ?? '',
        phone,
        phoneDisplay,
      ].join(' ').toLowerCase();
      final matchesQuery =
          normalizedQuery.isEmpty || searchable.contains(normalizedQuery);
      return matchesTenant && matchesDepartment && matchesQuery;
    }).toList();
  }

  TenantSummary? _findTenantBySlug(String slug) {
    for (final tenant in mockTenants) {
      if (tenant.slug == slug) {
        return tenant;
      }
    }
    return null;
  }
}

class _FakeEmployeeDetailRepository implements EmployeeDetailRepository {
  const _FakeEmployeeDetailRepository();

  @override
  Future<EmployeeDetail> loadEmployeeDetail(String employeeId) async {
    final employee = mockEmployees.firstWhere((item) => item.id == employeeId);
    return EmployeeDetail(
      id: employee.id,
      name: employee.name,
      phoneNumber: employee.phoneNumber,
      phoneDisplay: employee.phoneDisplay,
      email: employee.email,
      tenantId: employee.tenantId,
      tenantName: employee.tenantName,
      tenantSlug: employee.tenantSlug,
      department: employee.department,
      grade: employee.grade,
      position: employee.position,
      jobTitle: employee.jobTitle,
      status: employee.status,
      profileImageUrl: employee.profileImageUrl,
      sortOrder: employee.sortOrder,
      isManager: employee.isManager,
      joinedTenants: [
        TenantRef(
          id: employee.tenantId,
          name: employee.tenantName,
          slug: employee.tenantSlug,
          type: 'COMPANY',
        ),
      ],
      actions: const EmployeeActions(call: true, sms: true, email: true),
    );
  }
}

class _EnrichedEmployeeDetailRepository implements EmployeeDetailRepository {
  const _EnrichedEmployeeDetailRepository();

  @override
  Future<EmployeeDetail> loadEmployeeDetail(String employeeId) async {
    return const EmployeeDetail(
      id: 'user-001',
      name: '김하늘',
      phoneNumber: '+821012345678',
      phoneDisplay: '010-1234-5678',
      email: 'kim.haneul@corp.example.com',
      tenantId: 'tenant-hanmac',
      tenantName: '한맥',
      tenantSlug: 'hanmac',
      department: '기술연구소',
      grade: '책임',
      position: '팀장',
      jobTitle: '보안 아키텍트',
      status: 'active',
      joinedTenants: [
        TenantRef(
          id: 'tenant-hanmac',
          name: '한맥',
          slug: 'hanmac',
          type: 'COMPANY',
        ),
        TenantRef(
          id: 'tenant-rnd',
          name: '기술연구소',
          slug: 'rnd',
          type: 'ORGANIZATION',
          parentId: 'tenant-hanmac',
        ),
      ],
      actions: EmployeeActions(call: true, sms: true, email: true),
    );
  }
}

class _FailingEmployeeDetailRepository implements EmployeeDetailRepository {
  const _FailingEmployeeDetailRepository();

  @override
  Future<EmployeeDetail> loadEmployeeDetail(String employeeId) async {
    throw const ApiException(
      statusCode: 500,
      apiError: ApiError(
        error: 'detail_failed',
        code: 'detail_failed',
        details: {},
      ),
    );
  }
}

class _ActionRestrictedEmployeeDetailRepository
    implements EmployeeDetailRepository {
  const _ActionRestrictedEmployeeDetailRepository();

  @override
  Future<EmployeeDetail> loadEmployeeDetail(String employeeId) async {
    return const EmployeeDetail(
      id: 'user-001',
      name: '김하늘',
      phoneNumber: '+821012345678',
      phoneDisplay: '010-1234-5678',
      email: 'haneul.kim@example.com',
      tenantId: 'tenant-hanmac',
      tenantName: '한맥',
      tenantSlug: 'hanmac',
      department: '기술연구소',
      grade: '책임',
      position: '팀장',
      jobTitle: 'Flutter 개발',
      status: 'active',
      joinedTenants: [],
      actions: EmployeeActions(call: false, sms: true, email: true),
    );
  }
}

class _CountingDirectoryRepository implements DirectoryRepository {
  var calls = 0;

  @override
  Future<List<Employee>> loadEmployees(DirectoryQuery query) async {
    calls += 1;
    return const _FakeDirectoryRepository().loadEmployees(query);
  }
}

class _FakeOrganizationRepository implements OrganizationRepository {
  const _FakeOrganizationRepository();

  @override
  Future<OrgChartSnapshot> getOrgChart({
    String? tenantId,
    bool refresh = false,
  }) async {
    return OrgChartSnapshot(
      tenants: mockTenants,
      employees: mockEmployees,
      generatedAt: DateTime.parse('2026-07-02T00:00:00Z'),
    );
  }

  @override
  Future<TenantListResponse> listTenants() async {
    return TenantListResponse(
      items: mockTenants,
      generatedAt: DateTime.parse('2026-07-02T00:00:00Z'),
    );
  }
}

class _FakeFavoritesRepository implements FavoritesRepository {
  _FakeFavoritesRepository()
    : _favorites = [
        FavoriteEmployee(
          employeeId: 'user-001',
          createdAt: DateTime.parse('2026-07-02T00:00:00Z'),
        ),
      ];

  final List<FavoriteEmployee> _favorites;

  @override
  Future<List<FavoriteEmployee>> loadFavorites() async {
    return List.unmodifiable(_favorites);
  }

  @override
  Future<void> toggleFavorite(String employeeId) async {
    final existingIndex = _favorites.indexWhere(
      (favorite) => favorite.employeeId == employeeId,
    );
    if (existingIndex >= 0) {
      _favorites.removeAt(existingIndex);
      return;
    }

    _favorites.add(
      FavoriteEmployee(employeeId: employeeId, createdAt: DateTime.now()),
    );
  }
}

class _UnauthorizedDirectoryRepository implements DirectoryRepository {
  const _UnauthorizedDirectoryRepository();

  @override
  Future<List<Employee>> loadEmployees(DirectoryQuery query) async {
    throw const ApiException(
      statusCode: 401,
      apiError: ApiError(
        error: 'unauthorized',
        code: 'unauthorized',
        details: {},
      ),
    );
  }
}

class _DuplicateAffiliationDirectoryRepository implements DirectoryRepository {
  const _DuplicateAffiliationDirectoryRepository();

  @override
  Future<List<Employee>> loadEmployees(DirectoryQuery query) async {
    return const [
      Employee(
        id: 'is3-user',
        name: '송대일',
        phoneNumber: '+821012345678',
        phoneDisplay: '+821012345678',
        tenantId: 'tenant-is3',
        tenantName: 'IS3',
        tenantSlug: 'is3',
        department: 'IS3',
      ),
    ];
  }
}

class _OrganizationScopedDirectoryRepository implements DirectoryRepository {
  const _OrganizationScopedDirectoryRepository();

  @override
  Future<List<Employee>> loadEmployees(DirectoryQuery query) async {
    if (query.tenantSlug == 'hanmac' && query.query == '+821012345678') {
      return const [
        Employee(
          id: 'user-002',
          name: '박서준',
          phoneNumber: '+821012345678',
          phoneDisplay: '010-1234-5678',
          email: 'seojun.park@example.com',
          tenantId: 'tenant-ict',
          tenantName: 'ICT사업부',
          tenantSlug: 'ict',
          department: 'ICT사업부',
          position: '매니저',
        ),
      ];
    }

    if (query.tenantSlug == 'ict') {
      return const [
        Employee(
          id: 'user-002',
          name: '박서준',
          phoneNumber: '+821023456789',
          phoneDisplay: '010-2345-6789',
          email: 'seojun.park@example.com',
          tenantId: 'tenant-ict',
          tenantName: 'ICT사업부',
          tenantSlug: 'ict',
          department: 'ICT사업부',
          grade: '선임',
          position: '매니저',
        ),
      ];
    }

    if (query.tenantSlug == 'rnd') {
      return const [
        Employee(
          id: 'user-001',
          name: '김하늘',
          phoneNumber: '+821012345678',
          phoneDisplay: '010-1234-5678',
          email: 'haneul.kim@example.com',
          tenantId: 'tenant-rnd',
          tenantName: '기술연구소',
          tenantSlug: 'rnd',
          department: '기술연구소',
          grade: '책임',
          position: '팀장',
        ),
        Employee(
          id: 'user-005',
          name: '최민준',
          phoneNumber: '',
          phoneDisplay: null,
          email: 'minjun.choi@example.com',
          tenantId: 'tenant-rnd',
          tenantName: '기술연구소',
          tenantSlug: 'rnd',
          department: '기술연구소',
          grade: '수석',
          position: '연구원',
        ),
      ];
    }

    if (query.tenantSlug == 'ict' && query.query.isEmpty) {
      return const [
        Employee(
          id: 'user-002',
          name: '박서준',
          phoneNumber: '+821023456789',
          phoneDisplay: '010-2345-6789',
          email: 'seojun.park@example.com',
          tenantId: 'tenant-ict',
          tenantName: 'ICT사업부',
          tenantSlug: 'ict',
          department: 'ICT사업부',
          grade: '선임',
          position: '매니저',
          jobTitle: 'API 연동',
          status: 'active',
          sortOrder: 20,
        ),
      ];
    }

    return const _FakeDirectoryRepository().loadEmployees(query);
  }
}

class _OrganizationSortingDirectoryRepository implements DirectoryRepository {
  const _OrganizationSortingDirectoryRepository();

  @override
  Future<List<Employee>> loadEmployees(DirectoryQuery query) async {
    if (query.tenantSlug != 'rnd') {
      return const _FakeDirectoryRepository().loadEmployees(query);
    }
    return const [
      Employee(
        id: 'user-b',
        name: '유서진',
        phoneNumber: '+821011111113',
        phoneDisplay: '010-1111-1113',
        email: 'user3@example.com',
        tenantId: 'tenant-infra-bim3',
        tenantName: '인프라 BIM3',
        tenantSlug: 'rnd',
        department: '미지정',
        grade: '사원',
        position: '연구원',
      ),
      Employee(
        id: 'user-director',
        name: '박도현',
        phoneNumber: '+821011111115',
        phoneDisplay: '010-1111-1115',
        email: 'director@example.com',
        tenantId: 'tenant-infra-bim3',
        tenantName: '인프라 BIM3',
        tenantSlug: 'rnd',
        department: '미지정',
        grade: '이사',
        position: '임원',
      ),
      Employee(
        id: 'user-a',
        name: '김성엽',
        phoneNumber: '+821011111112',
        phoneDisplay: '010-1111-1112',
        email: 'user2@example.com',
        tenantId: 'tenant-infra-bim3',
        tenantName: '인프라 BIM3',
        tenantSlug: 'rnd',
        department: '미지정',
        grade: '수석연구원',
        position: '연구원',
      ),
      Employee(
        id: 'user-leader',
        name: '김지영',
        phoneNumber: '+821011111111',
        phoneDisplay: '010-1111-1111',
        email: 'leader@example.com',
        tenantId: 'tenant-infra-bim3',
        tenantName: '인프라 BIM3',
        tenantSlug: 'rnd',
        department: '미지정',
        grade: '책임연구원',
        position: '팀장',
        isManager: true,
      ),
      Employee(
        id: 'user-c',
        name: '한반석',
        phoneNumber: '+821011111114',
        phoneDisplay: '010-1111-1114',
        email: 'user4@example.com',
        tenantId: 'tenant-infra-bim3',
        tenantName: '인프라 BIM3',
        tenantSlug: 'rnd',
        department: '미지정',
        grade: '선임연구원',
        position: '연구원',
      ),
    ];
  }
}

class _DirectAffiliationDirectoryRepository implements DirectoryRepository {
  const _DirectAffiliationDirectoryRepository();

  @override
  Future<List<Employee>> loadEmployees(DirectoryQuery query) async {
    if (query.tenantSlug != 'hanmac') {
      return const _FakeDirectoryRepository().loadEmployees(query);
    }
    return const [
      Employee(
        id: 'direct-manager',
        name: '이영희',
        phoneNumber: '+821011112222',
        phoneDisplay: '010-1111-2222',
        email: 'younghee.lee@example.com',
        tenantId: 'tenant-hanmac',
        tenantName: '한맥',
        tenantSlug: 'hanmac',
        grade: '이사',
        position: '디비전장',
        isManager: true,
      ),
      Employee(
        id: 'child-member',
        name: '김하늘',
        phoneNumber: '+821012345678',
        phoneDisplay: '010-1234-5678',
        email: 'haneul.kim@example.com',
        tenantId: 'tenant-hanmac',
        tenantName: '한맥',
        tenantSlug: 'hanmac',
        department: '기술연구소',
        grade: '책임',
        position: '팀장',
      ),
    ];
  }
}

class _OverlappingMembershipDirectoryRepository implements DirectoryRepository {
  const _OverlappingMembershipDirectoryRepository();

  @override
  Future<List<Employee>> loadEmployees(DirectoryQuery query) async {
    if (query.tenantSlug != 'hanmac') {
      return const _FakeDirectoryRepository().loadEmployees(query);
    }
    return const [
      Employee(
        id: 'root-only-exec',
        name: '이영희',
        phoneNumber: '+821011112222',
        phoneDisplay: '010-1111-2222',
        email: 'younghee.lee@example.com',
        tenantId: 'tenant-hanmac',
        tenantName: '한맥',
        tenantSlug: 'hanmac',
        grade: '이사',
        position: '디비전장',
        isManager: true,
      ),
      Employee(
        id: 'child-member-1',
        name: '김하늘',
        phoneNumber: '+821012345678',
        phoneDisplay: '010-1234-5678',
        email: 'haneul.kim@example.com',
        tenantId: 'tenant-hanmac',
        tenantName: '한맥',
        tenantSlug: 'hanmac',
        grade: '책임',
        position: '팀장',
      ),
      Employee(
        id: 'child-member-1',
        name: '김하늘',
        phoneNumber: '+821012345678',
        phoneDisplay: '010-1234-5678',
        email: 'haneul.kim@example.com',
        tenantId: 'tenant-rnd',
        tenantName: '한맥',
        tenantSlug: 'rnd',
        department: '기술연구소',
        grade: '책임',
        position: '팀장',
      ),
      Employee(
        id: 'child-member-2',
        name: '박서준',
        phoneNumber: '+821023456789',
        phoneDisplay: '010-2345-6789',
        email: 'seojun.park@example.com',
        tenantId: 'tenant-hanmac',
        tenantName: '한맥',
        tenantSlug: 'hanmac',
        grade: '선임',
        position: '매니저',
      ),
      Employee(
        id: 'child-member-2',
        name: '박서준',
        phoneNumber: '+821023456789',
        phoneDisplay: '010-2345-6789',
        email: 'seojun.park@example.com',
        tenantId: 'tenant-ict',
        tenantName: '한맥',
        tenantSlug: 'ict',
        department: 'ICT사업부',
        grade: '선임',
        position: '매니저',
      ),
    ];
  }
}

class _BroadSearchDirectoryRepository implements DirectoryRepository {
  const _BroadSearchDirectoryRepository();

  @override
  Future<List<Employee>> loadEmployees(DirectoryQuery query) async {
    if (query.tenantSlug != 'hanmac' && query.apiDepartment != '통합시스템') {
      return const _FakeDirectoryRepository().loadEmployees(query);
    }

    return const [
      Employee(
        id: 'broad-001',
        name: '김윤재',
        phoneNumber: '+821097479838',
        phoneDisplay: '010-9747-9838',
        email: 'yoonjae.kim@example.com',
        tenantId: 'tenant-hanmac',
        tenantName: '총괄기획&기술개발센터',
        tenantSlug: 'hanmac',
        department: '통합시스템',
        position: 'IS1',
      ),
      Employee(
        id: 'broad-002',
        name: '한승민',
        phoneNumber: '+821031891514',
        phoneDisplay: '010-3189-1514',
        email: 'seungmin.han@example.com',
        tenantId: 'tenant-hanmac',
        tenantName: '총괄기획&기술개발센터',
        tenantSlug: 'hanmac',
        department: '통합시스템',
        position: 'IS1',
      ),
      Employee(
        id: 'broad-003',
        name: '권오재',
        phoneNumber: '+821091143943',
        phoneDisplay: '010-9114-3943',
        email: 'ojae.kwon@example.com',
        tenantId: 'tenant-hanmac',
        tenantName: '총괄기획&기술개발센터',
        tenantSlug: 'hanmac',
        department: '통합시스템',
        position: 'IS2',
      ),
    ];
  }
}

class _FakeContactLauncher implements ContactLauncher {
  _FakeContactLauncher();

  final List<String> calledNumbers = [];

  @override
  Future<bool> call(String phoneNumber) async {
    calledNumbers.add('call:$phoneNumber');
    return true;
  }

  @override
  Future<bool> sms(String phoneNumber) async {
    calledNumbers.add('sms:$phoneNumber');
    return true;
  }
}
