// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';

import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart' as io;
import 'package:flutter_tools/src/base/net.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:quiver/testing/async.dart';

import '../../src/common.dart';
import '../../src/context.dart';

void main() {
  group('successful fetch', () {
    const String responseString = 'response string';
    List<int> responseData;

    setUp(() {
      responseData = utf8.encode(responseString);
    });

    testUsingContext('fetchUrl() gets the data', () async {
      final List<int> data = await fetchUrl(Uri.parse('http://example.invalid/'));
      expect(data, equals(responseData));
    }, overrides: <Type, Generator>{
      HttpClientFactory: () => () => FakeHttpClient(200, data: responseString),
    });

    testUsingContext('fetchUrl(destFile) writes the data to a file', () async {
      final File destFile = fs.file('dest_file')..createSync();
      final List<int> data = await fetchUrl(
        Uri.parse('http://example.invalid/'),
        destFile: destFile,
      );
      expect(data, equals(<int>[]));
      expect(destFile.readAsStringSync(), equals(responseString));
    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem(),
      HttpClientFactory: () => () => FakeHttpClient(200, data: responseString),
      ProcessManager: () => FakeProcessManager.any(),
    });
  });

  testUsingContext('retry from 500', () async {
    String error;
    FakeAsync().run((FakeAsync time) {
      fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
        error = 'test completed unexpectedly';
      }, onError: (dynamic exception) {
        error = 'test failed unexpectedly: $exception';
      });
      expect(testLogger.statusText, '');
      time.elapse(const Duration(milliseconds: 10000));
      expect(testLogger.statusText,
        'Download failed -- attempting retry 1 in 1 second...\n'
        'Download failed -- attempting retry 2 in 2 seconds...\n'
        'Download failed -- attempting retry 3 in 4 seconds...\n'
        'Download failed -- attempting retry 4 in 8 seconds...\n',
      );
    });
    expect(testLogger.errorText, isEmpty);
    expect(error, isNull);
  }, overrides: <Type, Generator>{
    HttpClientFactory: () => () => FakeHttpClient(500),
  });

  testUsingContext('retry from network error', () async {
    String error;
    FakeAsync().run((FakeAsync time) {
      fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
        error = 'test completed unexpectedly';
      }, onError: (dynamic exception) {
        error = 'test failed unexpectedly: $exception';
      });
      expect(testLogger.statusText, '');
      time.elapse(const Duration(milliseconds: 10000));
      expect(testLogger.statusText,
        'Download failed -- attempting retry 1 in 1 second...\n'
        'Download failed -- attempting retry 2 in 2 seconds...\n'
        'Download failed -- attempting retry 3 in 4 seconds...\n'
        'Download failed -- attempting retry 4 in 8 seconds...\n',
      );
    });
    expect(testLogger.errorText, isEmpty);
    expect(error, isNull);
  }, overrides: <Type, Generator>{
    HttpClientFactory: () => () => FakeHttpClient(200),
  });

  testUsingContext('retry from SocketException', () async {
    String error;
    FakeAsync().run((FakeAsync time) {
      fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
        error = 'test completed unexpectedly';
      }, onError: (dynamic exception) {
        error = 'test failed unexpectedly: $exception';
      });
      expect(testLogger.statusText, '');
      time.elapse(const Duration(milliseconds: 10000));
      expect(testLogger.statusText,
        'Download failed -- attempting retry 1 in 1 second...\n'
        'Download failed -- attempting retry 2 in 2 seconds...\n'
        'Download failed -- attempting retry 3 in 4 seconds...\n'
        'Download failed -- attempting retry 4 in 8 seconds...\n',
      );
    });
    expect(testLogger.errorText, isEmpty);
    expect(error, isNull);
    expect(testLogger.traceText, contains('Download error: SocketException'));
  }, overrides: <Type, Generator>{
    HttpClientFactory: () => () => FakeHttpClientThrowing(
      const io.SocketException('test exception handling'),
    ),
  });

  testUsingContext('no retry from HandshakeException', () async {
    String error;
    FakeAsync().run((FakeAsync time) {
      fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
        error = 'test completed unexpectedly';
      }, onError: (dynamic exception) {
        error = 'test failed: $exception';
      });
      expect(testLogger.statusText, '');
      time.elapse(const Duration(milliseconds: 10000));
      expect(testLogger.statusText, '');
    });
    expect(error, startsWith('test failed'));
    expect(testLogger.traceText, contains('HandshakeException'));
  }, overrides: <Type, Generator>{
    HttpClientFactory: () => () => FakeHttpClientThrowing(
      const io.HandshakeException('test exception handling'),
    ),
  });

  testUsingContext('check for bad override on ArgumentError', () async {
    String error;
    FakeAsync().run((FakeAsync time) {
      fetchUrl(Uri.parse('example.invalid/')).then((List<int> value) {
        error = 'test completed unexpectedly';
      }, onError: (dynamic exception) {
        error = 'test failed: $exception';
      });
      expect(testLogger.statusText, '');
      time.elapse(const Duration(milliseconds: 10000));
      expect(testLogger.statusText, '');
    });
    expect(error, startsWith('test failed'));
    expect(testLogger.errorText, contains('Invalid argument'));
    expect(error, contains('FLUTTER_STORAGE_BASE_URL'));
  }, overrides: <Type, Generator>{
    HttpClientFactory: () => () => FakeHttpClientThrowing(
      ArgumentError('test exception handling'),
    ),
    Platform: () => FakePlatform.fromPlatform(const LocalPlatform())
      ..environment = <String, String>{
        'FLUTTER_STORAGE_BASE_URL': 'example.invalid',
      },
  });

  testUsingContext('retry from HttpException', () async {
    String error;
    FakeAsync().run((FakeAsync time) {
      fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
        error = 'test completed unexpectedly';
      }, onError: (dynamic exception) {
        error = 'test failed unexpectedly: $exception';
      });
      expect(testLogger.statusText, '');
      time.elapse(const Duration(milliseconds: 10000));
      expect(testLogger.statusText,
        'Download failed -- attempting retry 1 in 1 second...\n'
        'Download failed -- attempting retry 2 in 2 seconds...\n'
        'Download failed -- attempting retry 3 in 4 seconds...\n'
        'Download failed -- attempting retry 4 in 8 seconds...\n',
      );
    });
    expect(testLogger.errorText, isEmpty);
    expect(error, isNull);
    expect(testLogger.traceText, contains('Download error: HttpException'));
  }, overrides: <Type, Generator>{
    HttpClientFactory: () => () => FakeHttpClientThrowing(
      const io.HttpException('test exception handling'),
    ),
  });

  testUsingContext('retry from HttpException when request throws', () async {
    String error;
    FakeAsync().run((FakeAsync time) {
      fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
        error = 'test completed unexpectedly';
      }, onError: (dynamic exception) {
        error = 'test failed unexpectedly: $exception';
      });
      expect(testLogger.statusText, '');
      time.elapse(const Duration(milliseconds: 10000));
      expect(testLogger.statusText,
        'Download failed -- attempting retry 1 in 1 second...\n'
        'Download failed -- attempting retry 2 in 2 seconds...\n'
        'Download failed -- attempting retry 3 in 4 seconds...\n'
        'Download failed -- attempting retry 4 in 8 seconds...\n',
      );
    });
    expect(testLogger.errorText, isEmpty);
    expect(error, isNull);
    expect(testLogger.traceText, contains('Download error: HttpException'));
  }, overrides: <Type, Generator>{
    HttpClientFactory: () => () => FakeHttpClientThrowingRequest(
      const io.HttpException('test exception handling'),
    ),
  });

  testUsingContext('max attempts', () async {
    String error;
    List<int> actualResult;
    FakeAsync().run((FakeAsync time) {
      fetchUrl(Uri.parse('http://example.invalid/'), maxAttempts: 3).then((List<int> value) {
        actualResult = value;
      }, onError: (dynamic exception) {
        error = 'test failed unexpectedly: $exception';
      });
      expect(testLogger.statusText, '');
      time.elapse(const Duration(milliseconds: 10000));
      expect(testLogger.statusText,
        'Download failed -- attempting retry 1 in 1 second...\n'
        'Download failed -- attempting retry 2 in 2 seconds...\n'
        'Download failed -- retry 3\n',
      );
    });
    expect(testLogger.errorText, isEmpty);
    expect(error, isNull);
    expect(actualResult, isNull);
  }, overrides: <Type, Generator>{
    HttpClientFactory: () => () => FakeHttpClient(500),
  });

  testUsingContext('remote file non-existant', () async {
    final Uri invalid = Uri.parse('http://example.invalid/');
    final bool result = await doesRemoteFileExist(invalid);
    expect(result, false);
  }, overrides: <Type, Generator>{
    HttpClientFactory: () => () => FakeHttpClient(404),
  });

  testUsingContext('remote file server error', () async {
    final Uri valid = Uri.parse('http://example.valid/');
    final bool result = await doesRemoteFileExist(valid);
    expect(result, false);
  }, overrides: <Type, Generator>{
    HttpClientFactory: () => () => FakeHttpClient(500),
  });

  testUsingContext('remote file exists', () async {
    final Uri valid = Uri.parse('http://example.valid/');
    final bool result = await doesRemoteFileExist(valid);
    expect(result, true);
  }, overrides: <Type, Generator>{
    HttpClientFactory: () => () => FakeHttpClient(200),
  });
}

class FakeHttpClientThrowing implements io.HttpClient {
  FakeHttpClientThrowing(this.exception);

  final Object exception;

  @override
  Future<io.HttpClientRequest> getUrl(Uri url) async {
    throw exception;
  }

  @override
  dynamic noSuchMethod(Invocation invocation) {
    throw 'io.HttpClient - $invocation';
  }
}

class FakeHttpClient implements io.HttpClient {
  FakeHttpClient(this.statusCode, { this.data });

  final int statusCode;
  final String data;

  @override
  Future<io.HttpClientRequest> getUrl(Uri url) async {
    return FakeHttpClientRequest(statusCode, data: data);
  }

  @override
  Future<io.HttpClientRequest> headUrl(Uri url) async {
    return FakeHttpClientRequest(statusCode);
  }

  @override
  dynamic noSuchMethod(Invocation invocation) {
    throw 'io.HttpClient - $invocation';
  }
}

class FakeHttpClientThrowingRequest implements io.HttpClient {
  FakeHttpClientThrowingRequest(this.exception);

  final Object exception;

  @override
  Future<io.HttpClientRequest> getUrl(Uri url) async {
    return FakeHttpClientRequestThrowing(exception);
  }

  @override
  dynamic noSuchMethod(Invocation invocation) {
    throw 'io.HttpClient - $invocation';
  }
}

class FakeHttpClientRequest implements io.HttpClientRequest {
  FakeHttpClientRequest(this.statusCode, { this.data });

  final int statusCode;
  final String data;

  @override
  Future<io.HttpClientResponse> close() async {
    return FakeHttpClientResponse(statusCode, data: data);
  }

  @override
  dynamic noSuchMethod(Invocation invocation) {
    throw 'io.HttpClientRequest - $invocation';
  }
}

class FakeHttpClientRequestThrowing implements io.HttpClientRequest {
  FakeHttpClientRequestThrowing(this.exception);

  final Object exception;

  @override
  Future<io.HttpClientResponse> close() async {
    throw exception;
  }

  @override
  dynamic noSuchMethod(Invocation invocation) {
    throw 'io.HttpClientRequest - $invocation';
  }
}

class FakeHttpClientResponse implements io.HttpClientResponse {
  FakeHttpClientResponse(this.statusCode, { this.data });

  @override
  final int statusCode;

  final String data;

  @override
  String get reasonPhrase => '<reason phrase>';

  @override
  StreamSubscription<Uint8List> listen(
    void onData(Uint8List event), {
    Function onError,
    void onDone(),
    bool cancelOnError,
  }) {
    if (data == null) {
      return Stream<Uint8List>.fromFuture(Future<Uint8List>.error(
        const io.SocketException('test'),
      )).listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
    } else {
      return Stream<Uint8List>.fromFuture(Future<Uint8List>.value(
        utf8.encode(data) as Uint8List,
      )).listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
    }
  }

  @override
  Future<dynamic> forEach(void Function(Uint8List element) action) async {
    if (data == null) {
      return Future<void>.error(const io.SocketException('test'));
    } else {
      return Future<void>.microtask(() => action(utf8.encode(data)));
    }
  }

  @override
  dynamic noSuchMethod(Invocation invocation) {
    throw 'io.HttpClientResponse - $invocation';
  }
}
