// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:file/file.dart';
import 'package:meta/meta.dart';

import 'common.dart';
import 'mutable_recording.dart';
import 'recording.dart';
import 'recording_directory.dart';
import 'recording_file.dart';
import 'recording_link.dart';
import 'recording_proxy_mixin.dart';
import 'replay_file_system.dart';

/// File system that records invocations for later playback in tests.
///
/// This will record all invocations (methods, property getters, and property
/// setters) that occur on it, in an opaque format that can later be used in
/// [ReplayFileSystem]. All activity in the [File], [Directory], [Link],
/// [IOSink], and [RandomAccessFile] instances returned from this API will also
/// be recorded.
///
/// This class is intended for use in tests, where you would otherwise have to
/// set up complex mocks or fake file systems. With this class, the process is
/// as follows:
///
///   - You record the file system activity during a real run of your program
///     by injecting a `RecordingFileSystem` that delegates to your real file
///     system.
///   - You serialize that recording to disk as your program finishes.
///   - You use that recording in tests to create a mock file system that knows
///     how to respond to the exact invocations your program makes. Any
///     invocations that aren't in the recording will throw, and you can make
///     assertions in your tests about which methods were invoked and in what
///     order.
///
/// *Implementation note*: this class uses [noSuchMethod] to dynamically handle
/// invocations. As a result, method references on objects herein will not pass
/// `is` checks or checked-mode checks on type. For example:
///
/// ```dart
/// typedef FileStat StatSync(String path);
/// FileSystem fs = new RecordingFileSystem(delegate: delegate, destination: dir);
///
/// StatSync method = fs.statSync;     // Will fail in checked-mode
/// fs.statSync is StatSync            // Will return false
/// fs.statSync is Function            // Will return false
///
/// dynamic method2 = fs.statSync;     // OK
/// FileStat stat = method2('/path');  // OK
/// ```
///
/// See also:
///   - [ReplayFileSystem]
abstract class RecordingFileSystem extends FileSystem {
  /// Creates a new `RecordingFileSystem`.
  ///
  /// Invocations will be recorded and forwarded to the specified [delegate]
  /// file system.
  ///
  /// The recording will be serialized to the specified [destination] directory
  /// (only when `flush` is called on this file system's [recording]).
  ///
  /// If [stopwatch] is specified, it will be assumed to have already been
  /// started by the caller, and it will be used to record timestamps on each
  /// recorded invocation. If `stopwatch` is unspecified (or `null`), a new
  /// stopwatch will be created and started immediately to record these
  /// timestamps.
  factory RecordingFileSystem({
    @required FileSystem delegate,
    @required Directory destination,
    Stopwatch stopwatch,
  }) =>
      new RecordingFileSystemImpl(delegate, destination, stopwatch);

  /// The file system to which invocations will be forwarded upon recording.
  FileSystem get delegate;

  /// The recording generated by invocations on this file system.
  ///
  /// The recording provides access to the invocation events that have been
  /// recorded thus far, as well as the ability to flush them to disk.
  LiveRecording get recording;

  /// The stopwatch used to record timestamps on invocation events.
  ///
  /// Timestamps will be recorded before the delegate is invoked (not after
  /// the delegate returns).
  Stopwatch get stopwatch;
}

/// Non-exported implementation class for `RecordingFileSystem`.
class RecordingFileSystemImpl extends FileSystem
    with RecordingProxyMixin
    implements RecordingFileSystem {
  /// Creates a new `RecordingFileSystemImpl`.
  RecordingFileSystemImpl(
      this.delegate, Directory destination, Stopwatch recordingStopwatch)
      : recording = new MutableRecording(destination),
        stopwatch = recordingStopwatch ?? new Stopwatch() {
    if (recordingStopwatch == null) {
      // We instantiated our own stopwatch, so start it ourselves.
      stopwatch.start();
    }

    methods.addAll(<Symbol, Function>{
      #directory: _directory,
      #file: _file,
      #link: _link,
      #stat: delegate.stat,
      #statSync: delegate.statSync,
      #identical: delegate.identical,
      #identicalSync: delegate.identicalSync,
      #type: delegate.type,
      #typeSync: delegate.typeSync,
    });

    properties.addAll(<Symbol, Function>{
      #path: () => delegate.path,
      #systemTempDirectory: _getSystemTempDirectory,
      #currentDirectory: _getCurrentDirectory,
      const Symbol('currentDirectory='): _setCurrentDirectory,
      #isWatchSupported: () => delegate.isWatchSupported,
    });
  }

  @override
  String get identifier => kFileSystemEncodedValue;

  /// The file system to which invocations will be forwarded upon recording.
  @override
  final FileSystem delegate;

  /// The recording generated by invocations on this file system.
  @override
  final MutableRecording recording;

  /// The stopwatch used to record timestamps on invocation events.
  @override
  final Stopwatch stopwatch;

  Directory _directory(dynamic path) =>
      new RecordingDirectory(this, delegate.directory(path));

  File _file(dynamic path) => new RecordingFile(this, delegate.file(path));

  Link _link(dynamic path) => new RecordingLink(this, delegate.link(path));

  Directory _getSystemTempDirectory() =>
      new RecordingDirectory(this, delegate.systemTempDirectory);

  Directory _getCurrentDirectory() =>
      new RecordingDirectory(this, delegate.currentDirectory);

  void _setCurrentDirectory(dynamic value) {
    delegate.currentDirectory = value;
  }
}
