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

import 'package:flutter_test/flutter_test.dart';

import 'package:appsok/src/models/jenkins_build.dart';
import 'package:appsok/src/services/artifact_staging_service.dart';

JenkinsBuild _makeBuild({int number = 1}) => JenkinsBuild(
  number: number,
  jobName: 'android-app',
  url: Uri.parse('https://jenkins.example/job/android-app/$number/'),
  startedAt: null,
  result: 'SUCCESS',
  artifacts: const [],
);

const _apk = BuildArtifact(
  fileName: 'app-qa.apk',
  relativePath: 'outputs/app-qa.apk',
);

void main() {
  late Directory tempDir;
  late ArtifactStagingService service;

  setUp(() async {
    tempDir = await Directory.systemTemp.createTemp('staging_test_');
    service = ArtifactStagingService(stagingDir: tempDir);
  });

  tearDown(() async {
    if (await tempDir.exists()) {
      await tempDir.delete(recursive: true);
    }
  });

  group('ArtifactStagingService.stageApk', () {
    test('stageApk creates file directly under staging root for slash job name', () async {
      final slashBuild = JenkinsBuild(
        number: 5,
        jobName: 'team/android-app',
        url: Uri.parse('https://jenkins.example/job/team/job/android-app/5/'),
        startedAt: null,
        result: 'SUCCESS',
        artifacts: const [],
      );
      final bytes = List<int>.filled(512, 1);
      final staged = await service.stageApk(
        build: slashBuild,
        artifact: _apk,
        byteStream: Stream.fromIterable([bytes]),
      );

      // file must be directly under tempDir — no subdirectory created
      expect(staged.path, startsWith(tempDir.path));
      expect(staged.path.replaceFirst(tempDir.path, '').split('/').length, 2);
      expect(File(staged.path).parent.path, tempDir.path);
    });

    test('stageApk creates file directly under staging root for backslash job name', () async {
      final bsBuild = JenkinsBuild(
        number: 6,
        jobName: r'team\android-app',
        url: Uri.parse('https://jenkins.example/job/team/android-app/6/'),
        startedAt: null,
        result: 'SUCCESS',
        artifacts: const [],
      );
      final bytes = List<int>.filled(512, 1);
      final staged = await service.stageApk(
        build: bsBuild,
        artifact: _apk,
        byteStream: Stream.fromIterable([bytes]),
      );

      expect(File(staged.path).parent.path, tempDir.path);
    });

    test('stageApk writes downloaded bytes into temp directory', () async {
      final bytes = List<int>.filled(1024, 0xAB);
      final staged = await service.stageApk(
        build: _makeBuild(),
        artifact: _apk,
        byteStream: Stream.fromIterable([bytes]),
      );

      expect(staged.path, isNotEmpty);
      expect(staged.sizeBytes, bytes.length);

      final file = File(staged.path);
      expect(await file.exists(), isTrue);
      expect(await file.length(), bytes.length);
    });

    test('stageApk deletes partial file when cancelled', () async {
      Stream<List<int>> errorStream() async* {
        yield List<int>.filled(256, 1);
        throw StateError('cancelled');
      }

      await expectLater(
        service.stageApk(
          build: _makeBuild(),
          artifact: _apk,
          byteStream: errorStream(),
        ),
        throwsA(isA<StateError>()),
      );

      final file = File('${tempDir.path}/android-app_1_app-qa.apk');
      expect(await file.exists(), isFalse);
    });

    test('stageApk deletes partial file on stream error', () async {
      Stream<List<int>> errorStream() async* {
        yield List<int>.filled(512, 1);
        throw Exception('network failure');
      }

      await expectLater(
        service.stageApk(
          build: _makeBuild(),
          artifact: _apk,
          byteStream: errorStream(),
        ),
        throwsException,
      );

      final file = File('${tempDir.path}/android-app_1_app-qa.apk');
      expect(await file.exists(), isFalse);
    });

    test('stageApk rejects non apk artifact filename', () async {
      const nonApk = BuildArtifact(
        fileName: 'app-qa.txt',
        relativePath: 'outputs/app-qa.txt',
      );

      await expectLater(
        service.stageApk(
          build: _makeBuild(),
          artifact: nonApk,
          byteStream: Stream.value([1, 2, 3]),
        ),
        throwsA(isA<ArtifactVerifyException>()),
      );

      final file = File('${tempDir.path}/android-app_1_app-qa.txt');
      expect(await file.exists(), isFalse);
    });
  });

  group('ArtifactStagingService.verify', () {
    test('verify rejects non apk extension', () async {
      final f = File('${tempDir.path}/app.txt');
      await f.writeAsBytes([1, 2, 3]);

      await expectLater(
        service.verify(f.path),
        throwsA(isA<ArtifactVerifyException>()),
      );
    });

    test('verify rejects zero byte file', () async {
      final f = File('${tempDir.path}/app.apk');
      await f.writeAsBytes([]);

      await expectLater(
        service.verify(f.path),
        throwsA(isA<ArtifactVerifyException>()),
      );
    });

    test('verify rejects missing file', () async {
      await expectLater(
        service.verify('${tempDir.path}/nonexistent.apk'),
        throwsA(isA<ArtifactVerifyException>()),
      );
    });

    test('verify accepts valid apk file', () async {
      final f = File('${tempDir.path}/app-release.apk');
      await f.writeAsBytes(List<int>.filled(2048, 0));

      final result = await service.verify(f.path);
      expect(result.path, f.path);
      expect(result.sizeBytes, 2048);
    });
  });

  group('ArtifactStagingService.cleanup', () {
    test('cleanup deletes existing staged file', () async {
      final f = File('${tempDir.path}/app.apk');
      await f.writeAsBytes([1, 2, 3]);

      await service.cleanup(f.path);
      expect(await f.exists(), isFalse);
    });

    test('cleanup is safe when file does not exist', () async {
      await expectLater(service.cleanup('${tempDir.path}/gone.apk'), completes);
    });
  });
}
