import 'dart:async';
import 'dart:convert';
import 'dart:io';

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

import 'package:appsok/src/features/console/console_page.dart';
import 'package:appsok/src/models/adb_device.dart';
import 'package:appsok/src/services/adb_service.dart';
import 'package:appsok/src/theme/app_theme.dart';

class _FakeProcess implements Process {
  final _stdout = StreamController<List<int>>();
  final _stderr = StreamController<List<int>>();
  final _exitCode = Completer<int>();

  bool killCalled = false;

  void emitStdout(String text) => _stdout.add(utf8.encode(text));
  void emitStderr(String text) => _stderr.add(utf8.encode(text));

  @override
  Stream<List<int>> get stdout => _stdout.stream;

  @override
  Stream<List<int>> get stderr => _stderr.stream;

  @override
  Future<int> get exitCode => _exitCode.future;

  @override
  bool kill([ProcessSignal signal = ProcessSignal.sigterm]) {
    killCalled = true;
    if (!_exitCode.isCompleted) _exitCode.complete(-15);
    _stdout.close();
    _stderr.close();
    return true;
  }

  @override
  int get pid => 4242;

  @override
  IOSink get stdin => throw UnimplementedError();
}

void main() {
  testWidgets('colors sample logcat rows by parsed level', (tester) async {
    await tester.pumpWidget(
      MaterialApp(
        theme: AppTheme.light(),
        home: const Scaffold(body: ConsolePage()),
      ),
    );

    const infoLine =
        '06-08 10:41:22.121 1832 2110 I ActivityTaskManager: START u0 com.toki.app/.MainActivity';
    const debugLine =
        '06-08 10:41:22.339 5521 5521 D AppSokInstall: install session committed';
    const warnLine =
        '06-08 10:41:22.921 5521 5540 W NetworkMonitor: vpn route not available, using usb install cache';
    const errorLine =
        '06-08 10:41:23.411 5521 5521 E Sample: placeholder error row for filter styling';

    expect(_textColor(tester, infoLine), const Color(0xFFBDEFA8));
    expect(_textColor(tester, debugLine), const Color(0xFF8DE1D7));
    expect(_textColor(tester, warnLine), const Color(0xFFFFD28A));
    expect(_textColor(tester, errorLine), const Color(0xFFFFA3A3));
  });

  testWidgets('filters visible rows by package tag or text query', (
    tester,
  ) async {
    await tester.pumpWidget(
      MaterialApp(
        theme: AppTheme.light(),
        home: const Scaffold(body: ConsolePage()),
      ),
    );

    const matchingLine =
        '06-08 10:41:23.105 5521 5540 I ArtifactStore: app-debug.apk verified';
    const hiddenLine =
        '06-08 10:41:22.121 1832 2110 I ActivityTaskManager: START u0 com.toki.app/.MainActivity';

    await tester.enterText(find.byType(TextField), 'artifactstore');
    await tester.pump();

    expect(find.text(matchingLine), findsOneWidget);
    expect(find.text(hiddenLine), findsNothing);

    await tester.tap(find.byTooltip('필터 초기화'));
    await tester.pump();

    expect(find.text(hiddenLine), findsOneWidget);
  });

  testWidgets('filters visible rows by log level segment', (tester) async {
    await tester.pumpWidget(
      MaterialApp(
        theme: AppTheme.light(),
        home: const Scaffold(body: ConsolePage()),
      ),
    );

    const debugLine =
        '06-08 10:41:22.339 5521 5521 D AppSokInstall: install session committed';
    const errorLine =
        '06-08 10:41:23.411 5521 5521 E Sample: placeholder error row for filter styling';

    await tester.tap(find.text('ERROR'));
    await tester.pump();

    expect(find.text(errorLine), findsOneWidget);
    expect(find.text(debugLine), findsNothing);

    await tester.tap(find.text('DEBUG'));
    await tester.pump();

    expect(find.text(debugLine), findsOneWidget);
    expect(find.text(errorLine), findsNothing);
  });

  testWidgets('filters live brief-format rows by log level segment', (
    tester,
  ) async {
    final fakeProcess = _FakeProcess();
    final session = AdbLogcatSession(fakeProcess);
    const device = AdbDevice(
      serial: 'R5CT90A1B2C',
      state: 'device',
      model: 'SM_S918N',
      product: 'dm3q',
    );

    await tester.pumpWidget(
      MaterialApp(
        theme: AppTheme.light(),
        home: Scaffold(
          body: ConsolePage(
            device: device,
            logcatStarter: ({serial}) async => session,
          ),
        ),
      ),
    );
    await tester.pump();

    fakeProcess.emitStdout('I/Keep: keep this line\n');
    fakeProcess.emitStdout('E/Crash: crash this line\n');
    await tester.pump(Duration.zero);

    await tester.tap(find.text('INFO'));
    await tester.pump();

    expect(find.text('I/Keep: keep this line'), findsOneWidget);
    expect(find.text('E/Crash: crash this line'), findsNothing);

    await tester.tap(find.text('ERROR'));
    await tester.pump();

    expect(find.text('I/Keep: keep this line'), findsNothing);
    expect(find.text('E/Crash: crash this line'), findsOneWidget);
  });

  testWidgets('appends live stream lines and stops session on dispose', (
    tester,
  ) async {
    final fakeProcess = _FakeProcess();
    final session = AdbLogcatSession(fakeProcess);

    const device = AdbDevice(
      serial: 'R5CT90A1B2C',
      state: 'device',
      model: 'SM_S918N',
      product: 'dm3q',
    );

    var startCalls = 0;
    Future<AdbLogcatSession> fakeStarter({String? serial}) async {
      startCalls++;
      expect(serial, device.serial);
      return session;
    }

    await tester.pumpWidget(
      MaterialApp(
        theme: AppTheme.light(),
        home: Scaffold(
          body: ConsolePage(device: device, logcatStarter: fakeStarter),
        ),
      ),
    );

    // Initial pump to run initState/startSession
    await tester.pump();

    expect(startCalls, 1);

    // Emit live log lines
    fakeProcess.emitStdout('I/test: hello live line\n');
    await tester.pump(Duration.zero);

    expect(find.text('I/test: hello live line'), findsOneWidget);

    // Check dispose kills process
    await tester.pumpWidget(
      MaterialApp(
        theme: AppTheme.light(),
        home: const Scaffold(body: SizedBox.shrink()),
      ),
    );

    expect(fakeProcess.killCalled, isTrue);
  });

  testWidgets('discards stale session when device changes during async start', (
    tester,
  ) async {
    final completerA = Completer<AdbLogcatSession>();
    final completerB = Completer<AdbLogcatSession>();

    final fakeProcessA = _FakeProcess();
    final sessionA = AdbLogcatSession(fakeProcessA);
    final fakeProcessB = _FakeProcess();
    final sessionB = AdbLogcatSession(fakeProcessB);

    const deviceA = AdbDevice(
      serial: 'device-A',
      state: 'device',
      model: 'Pixel_8',
      product: 'shiba',
    );
    const deviceB = AdbDevice(
      serial: 'device-B',
      state: 'device',
      model: 'Galaxy_S23',
      product: 'dm3q',
    );

    var startCalls = 0;
    Future<AdbLogcatSession> fakeStarter({String? serial}) async {
      startCalls++;
      if (serial == 'device-A') {
        return completerA.future;
      } else if (serial == 'device-B') {
        return completerB.future;
      }
      throw Exception('unexpected serial');
    }

    // 1. Render ConsolePage with deviceA, starter completerA is pending
    await tester.pumpWidget(
      MaterialApp(
        theme: AppTheme.light(),
        home: Scaffold(
          body: ConsolePage(device: deviceA, logcatStarter: fakeStarter),
        ),
      ),
    );

    // Initial pump to trigger initState
    await tester.pump();
    expect(startCalls, 1);

    // 2. Update widget to deviceB, starter completerB is pending
    await tester.pumpWidget(
      MaterialApp(
        theme: AppTheme.light(),
        home: Scaffold(
          body: ConsolePage(device: deviceB, logcatStarter: fakeStarter),
        ),
      ),
    );
    // didUpdateWidget triggers start for deviceB
    await tester.pump();
    expect(startCalls, 2);

    // 3. Complete starter B first
    completerB.complete(sessionB);
    await tester.pump(); // let completerB resolve
    await tester.pump(Duration.zero); // let stream listen trigger

    // Emit line from B
    fakeProcessB.emitStdout('I/test: hello from B\n');
    await tester.pump(Duration.zero);
    expect(find.text('I/test: hello from B'), findsOneWidget);

    // 4. Now complete starter A (which is stale)
    completerA.complete(sessionA);
    await tester.pump(); // let completerA resolve
    await tester.pump(Duration.zero);

    // A session must be stopped immediately
    expect(fakeProcessA.killCalled, isTrue);
  });

  testWidgets(
    'clears loading state when pending live session becomes sample mode',
    (tester) async {
      final completer = Completer<AdbLogcatSession>();
      final fakeProcess = _FakeProcess();
      final session = AdbLogcatSession(fakeProcess);
      const device = AdbDevice(
        serial: 'device-pending',
        state: 'device',
        model: 'Pixel_8',
        product: 'shiba',
      );

      await tester.pumpWidget(
        MaterialApp(
          theme: AppTheme.light(),
          home: Scaffold(
            body: ConsolePage(
              device: device,
              logcatStarter: ({serial}) => completer.future,
            ),
          ),
        ),
      );
      await tester.pump();

      expect(find.byType(CircularProgressIndicator), findsOneWidget);

      await tester.pumpWidget(
        MaterialApp(
          theme: AppTheme.light(),
          home: const Scaffold(body: ConsolePage()),
        ),
      );
      await tester.pump();

      expect(find.byType(CircularProgressIndicator), findsNothing);
      expect(find.text('device 미선택'), findsOneWidget);
      expect(find.textContaining('ActivityTaskManager'), findsOneWidget);

      completer.complete(session);
      await tester.pump();

      expect(fakeProcess.killCalled, isTrue);
    },
  );

  testWidgets('auto-scrolls to the newest log while running', (tester) async {
    final fakeProcess = _FakeProcess();
    final session = AdbLogcatSession(fakeProcess);
    const device = AdbDevice(
      serial: 'R5CT90A1B2C',
      state: 'device',
      model: 'SM_S918N',
      product: 'dm3q',
    );

    await tester.pumpWidget(
      MaterialApp(
        theme: AppTheme.light(),
        home: Scaffold(
          body: ConsolePage(
            device: device,
            logcatStarter: ({serial}) async => session,
          ),
        ),
      ),
    );
    await tester.pump();

    // Emit enough lines to make the list scrollable (e.g., 50 lines)
    for (var i = 0; i < 50; i++) {
      fakeProcess.emitStdout('I/test: line $i\n');
    }
    await tester.pump(Duration.zero);
    await tester.pumpAndSettle();

    // Verify it auto-scrolled to the bottom
    final listView = tester.widget<ListView>(find.byType(ListView));
    final controller = listView.controller;
    expect(controller, isNotNull);
    expect(
      controller!.position.pixels,
      equals(controller.position.maxScrollExtent),
    );
  });

  testWidgets('keeps user scroll position while paused', (tester) async {
    final fakeProcess = _FakeProcess();
    final session = AdbLogcatSession(fakeProcess);
    const device = AdbDevice(
      serial: 'R5CT90A1B2C',
      state: 'device',
      model: 'SM_S918N',
      product: 'dm3q',
    );

    await tester.pumpWidget(
      MaterialApp(
        theme: AppTheme.light(),
        home: Scaffold(
          body: ConsolePage(
            device: device,
            logcatStarter: ({serial}) async => session,
          ),
        ),
      ),
    );
    await tester.pump();

    // 1. Emit enough lines to scroll
    for (var i = 0; i < 50; i++) {
      fakeProcess.emitStdout('I/test: line $i\n');
    }
    await tester.pump(Duration.zero);
    await tester.pumpAndSettle();

    final listView = tester.widget<ListView>(find.byType(ListView));
    final controller = listView.controller!;
    final maxScrollExtentBeforePause = controller.position.maxScrollExtent;
    expect(controller.position.pixels, equals(maxScrollExtentBeforePause));

    // 2. Press pause button
    final pauseButtonFinder = find.byTooltip('일시정지');
    expect(pauseButtonFinder, findsOneWidget);
    await tester.tap(pauseButtonFinder);
    await tester.pumpAndSettle();

    // Verify tooltip and icon changed
    expect(find.byTooltip('재개'), findsOneWidget);

    // 3. User scrolls up slightly to some offset (e.g., 100.0)
    controller.jumpTo(100.0);
    await tester.pumpAndSettle();
    expect(controller.position.pixels, equals(100.0));

    // 4. Emit more lines while paused
    for (var i = 50; i < 60; i++) {
      fakeProcess.emitStdout('I/test: line $i\n');
    }
    await tester.pump(Duration.zero);
    await tester.pumpAndSettle();

    // Verify offset remains at 100.0 (does not scroll to bottom)
    expect(controller.position.pixels, equals(100.0));

    // 5. Tap resume (tooltip '재개')
    final resumeButtonFinder = find.byTooltip('재개');
    await tester.tap(resumeButtonFinder);
    await tester.pumpAndSettle();

    // Verify tooltip is '일시정지' again and it scrolled to bottom
    expect(find.byTooltip('일시정지'), findsOneWidget);
    expect(
      controller.position.pixels,
      equals(controller.position.maxScrollExtent),
    );
  });

  testWidgets('clear button removes captured live log lines', (tester) async {
    final fakeProcess = _FakeProcess();
    final session = AdbLogcatSession(fakeProcess);
    const device = AdbDevice(
      serial: 'R5CT90A1B2C',
      state: 'device',
      model: 'SM_S918N',
      product: 'dm3q',
    );

    await tester.pumpWidget(
      MaterialApp(
        theme: AppTheme.light(),
        home: Scaffold(
          body: ConsolePage(
            device: device,
            logcatStarter: ({serial}) async => session,
          ),
        ),
      ),
    );
    await tester.pump();

    fakeProcess.emitStdout('I/test: line before clear\n');
    await tester.pump(Duration.zero);
    expect(find.text('I/test: line before clear'), findsOneWidget);

    await tester.tap(find.byTooltip('지우기'));
    await tester.pump();

    expect(find.text('I/test: line before clear'), findsNothing);
  });

  testWidgets('copies currently visible live log lines to clipboard', (
    tester,
  ) async {
    final copiedTexts = <String>[];
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(SystemChannels.platform, (call) async {
          if (call.method == 'Clipboard.setData') {
            copiedTexts.add(
              (call.arguments as Map<Object?, Object?>)['text']! as String,
            );
          }
          return null;
        });
    addTearDown(() {
      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
          .setMockMethodCallHandler(SystemChannels.platform, null);
    });

    final fakeProcess = _FakeProcess();
    final session = AdbLogcatSession(fakeProcess);
    const device = AdbDevice(
      serial: 'R5CT90A1B2C',
      state: 'device',
      model: 'SM_S918N',
      product: 'dm3q',
    );

    await tester.pumpWidget(
      MaterialApp(
        theme: AppTheme.light(),
        home: Scaffold(
          body: ConsolePage(
            device: device,
            logcatStarter: ({serial}) async => session,
          ),
        ),
      ),
    );
    await tester.pump();

    fakeProcess.emitStdout('I/Keep: keep this line\n');
    fakeProcess.emitStdout('I/Drop: drop this line\n');
    await tester.pump(Duration.zero);

    // Apply a query filter so only one line is visible.
    await tester.enterText(find.byType(TextField), 'keep');
    await tester.pump();

    await tester.tap(find.byTooltip('로그 복사'));
    await tester.pump();

    expect(copiedTexts, ['I/Keep: keep this line']);
  });

  testWidgets('keeps export disabled without live log lines', (tester) async {
    // No device / sample placeholder state: export button is disabled.
    await tester.pumpWidget(
      MaterialApp(
        theme: AppTheme.light(),
        home: const Scaffold(body: ConsolePage()),
      ),
    );
    await tester.pump();

    final sampleCopyButton = tester.widget<IconButton>(
      find.ancestor(
        of: find.byIcon(Icons.content_copy),
        matching: find.byType(IconButton),
      ),
    );
    expect(sampleCopyButton.onPressed, isNull);

    // Live mode but no captured lines yet: still disabled.
    final fakeProcess = _FakeProcess();
    final session = AdbLogcatSession(fakeProcess);
    const device = AdbDevice(
      serial: 'R5CT90A1B2C',
      state: 'device',
      model: 'SM_S918N',
      product: 'dm3q',
    );

    await tester.pumpWidget(
      MaterialApp(
        theme: AppTheme.light(),
        home: Scaffold(
          body: ConsolePage(
            device: device,
            logcatStarter: ({serial}) async => session,
          ),
        ),
      ),
    );
    await tester.pump();

    final emptyLiveCopyButton = tester.widget<IconButton>(
      find.ancestor(
        of: find.byIcon(Icons.content_copy),
        matching: find.byType(IconButton),
      ),
    );
    expect(emptyLiveCopyButton.onPressed, isNull);
  });
}

Color? _textColor(WidgetTester tester, String text) {
  return tester.widget<Text>(find.text(text)).style?.color;
}
