/*******************************************************************************

    Base utilities (types, functions) used in tests

    The main type in this module is `TestDub`. `TestDub` is a class that
    inherits from `Dub` and inject dependencies in it to avoid relying on IO.
    First and foremost, by overriding `makePackageManager` and returning a
    `TestPackageManager` instead, we avoid hitting the local filesystem and
    instead present a view of the "local packages" that is fully in-memory.
    Likewise, by providing a `MockPackageSupplier`, we can imitate the behavior
    of the registry without relying on it.

    Leftover_IO:
    Note that reliance on IO was originally all over the place in the Dub
    codebase. For this reason, **new tests might find themselves doing I/O**.
    When that happens, one should isolate the place which does I/O and refactor
    the code to make dependency injection possible and practical.
    An example of this is any place calling `Package.load`, `readPackageRecipe`,
    or `Package.findPackageFile`.

    Supported_features:
    In order to make writing tests possible and practical, not every features
    where implemented in `TestDub`. Notably, path-based packages are not
    supported at the moment, as they would need a better filesystem abstraction.
    However, it would be desirable to add support for them at some point in the
    future.

    Writing_tests:
    `TestDub` exposes a few extra features to make writing tests easier.
    Ideally, those extra features should be kept to a minimum, as a convenient
    API for writing tests is likely to be a convenient API for library and
    application developers as well.
    It is expected that most tests will be centered about the `Project`,
    also known as the "main package" that is loaded and drives Dub's logic
    when common operations such as `dub build` are performed.
    A minimalistic and documented unittest can be found in this module,
    showing the various features of the test framework.

    Logging:
    Dub writes to stdout / stderr in various places. While it would be desirable
    to do dependency injection on it, the benefits brought by doing so currently
    doesn't justify the amount of work required. If unittests for some reason
    trigger messages being written to stdout/stderr, make sure that the logging
    functions are being used instead of bare `write` / `writeln`.

*******************************************************************************/

module dub.test.base;

version (unittest):

import std.array;
public import std.algorithm;

import dub.data.settings;
public import dub.dependency;
public import dub.dub;
public import dub.package_;
import dub.packagemanager;
import dub.packagesuppliers.packagesupplier;
import dub.project;

/// Example of a simple unittest for a project with a single dependency
unittest
{
    // `a` will be loaded as the project while `b` will be loaded
    // as a simple package. The recipe files can be in JSON or SDL format,
    // here we use both to demonstrate this.
    const a = `{ "name": "a", "dependencies": { "b": "~>1.0" } }`;
    const b = `name "b"`;

    // Enabling this would provide some more verbose output, which makes
    // debugging a failing unittest much easier.
    version (none) {
        enableLogging();
        scope(exit) disableLogging();
    }

    scope dub = new TestDub();
    // Let the `PackageManager` know about the `b` package
    dub.addTestPackage(b, Version("1.0.0"), PackageFormat.sdl);
    // And about our main package
    auto mainPackage = dub.addTestPackage(a, Version("1.0.0"));
    // `Dub.loadPackage` will set this package as the project
    // While not required, it follows the common Dub use case.
    dub.loadPackage(mainPackage);
    // This triggers the dependency resolution process that happens
    // when one does not have a selection file in the project.
    // Dub will resolve dependencies and generate the selection file
    // (in memory). If your test has set dependencies / no dependencies,
    // this will not be needed.
    dub.upgrade(UpgradeOptions.select);

    // Simple tests can be performed using the public API
    assert(dub.project.hasAllDependencies(), "project has missing dependencies");
    assert(dub.project.getDependency("b", true), "Missing 'b' dependency");
    // While it is important to make your tests fail before you make them pass,
    // as is common with TDD, it can also be useful to test simple assumptions
    // as part of your basic tests. Here we want to make sure `getDependency`
    // doesn't always return something regardless of its first argument.
    // Note that this package segments modules by categories, e.g. dependencies,
    // and tests are run serially in a module, so one may rely on previous tests
    // having passed to avoid repeating some assumptions.
    assert(dub.project.getDependency("no", true) is null, "Returned unexpected dependency");
}

// TODO: Remove and handle logging the same way we handle other IO
import dub.internal.logging;

public void enableLogging()
{
    setLogLevel(LogLevel.debug_);
}

public void disableLogging()
{
    setLogLevel(LogLevel.none);
}

/**
 * An instance of Dub that does not rely on the environment
 *
 * This instance of dub should not read any environment variables,
 * nor should it do any file IO, to make it usable and reliable in unittests.
 * Currently it reads environment variables but does not read the configuration.
 */
public class TestDub : Dub
{
    /// Forward to base constructor
    public this (string root = ".", PackageSupplier[] extras = null,
                 SkipPackageSuppliers skip = SkipPackageSuppliers.none)
    {
        super(root, extras, skip);
    }

    /// Avoid loading user configuration
    protected override Settings loadConfig(ref SpecialDirs dirs) const
    {
        // No-op
        return Settings.init;
    }

	///
	protected override PackageManager makePackageManager() const
	{
		return new TestPackageManager();
	}

    /// See `MockPackageSupplier` documentation for this class' implementation
    protected override PackageSupplier makePackageSupplier(string url) const
    {
        return new MockPackageSupplier(url);
    }

	/// Loads the package from the specified path as the main project package.
	public override void loadPackage(NativePath path)
	{
		assert(0, "Not implemented");
	}

	/// Loads a specific package as the main project package (can be a sub package)
	public override void loadPackage(Package pack)
	{
		m_project = new Project(m_packageManager, pack, new TestSelectedVersions());
	}

	/// Reintroduce parent overloads
	public alias loadPackage = Dub.loadPackage;

	/**
	 * Returns a fully typed `TestPackageManager`
	 *
	 * This exposes the fully typed `PackageManager`, so that client
	 * can call convenience functions on it directly.
	 */
	public override @property inout(TestPackageManager) packageManager() inout
	{
		return cast(inout(TestPackageManager)) this.m_packageManager;
	}

	/**
	 * Creates a package with the provided recipe
	 *
	 * This is a convenience function provided to create a package based on
	 * a given recipe. This is to allow test-cases to be written based off
	 * issues more easily.
     *
     * In order for the `Package` to be visible to `Dub`, use `addTestPackage`,
     * as `makeTestPackage` simply creates the `Package` without adding it.
	 *
	 * Params:
	 *	 str = The string representation of the `PackageRecipe`
	 *	 recipe = The `PackageRecipe` to use
	 *	 vers = The version the package is at, e.g. `Version("1.0.0")`
	 *	 fmt = The format `str` is in, either JSON or SDL
	 *
	 * Returns:
	 *	 The created `Package` instance
	 */
	public Package makeTestPackage(string str, Version vers, PackageFormat fmt = PackageFormat.json)
	{
		import dub.recipe.io;
		final switch (fmt) {
			case PackageFormat.json:
				auto recipe = parsePackageRecipe(str, "dub.json");
                recipe.version_ = vers.toString();
                return new Package(recipe);
			case PackageFormat.sdl:
				auto recipe = parsePackageRecipe(str, "dub.sdl");
                recipe.version_ = vers.toString();
                return new Package(recipe);
		}
	}

    /// Ditto
	public Package addTestPackage(string str, Version vers, PackageFormat fmt = PackageFormat.json)
    {
        return this.packageManager.add(this.makeTestPackage(str, vers, fmt));
    }
}

/**
 *
 */
public class TestSelectedVersions : SelectedVersions {
	import dub.recipe.selection;

	/// Forward to parent's constructor
	public this(uint version_ = FileVersion) @safe pure nothrow @nogc
	{
		super(version_);
	}

	/// Ditto
	public this(Selected data) @safe pure nothrow @nogc
	{
		super(data);
	}

	/// Do not do IO
	public override void save(NativePath path)
	{
		// No-op
	}
}

/**
 * A `PackageManager` suitable to be used in unittests
 *
 * This `PackageManager` does not perform any IO. It imitates the base
 * `PackageManager`, exposing 3 locations, but loading of packages is not
 * automatic and needs to be done by passing a `Package` instance.
 */
package class TestPackageManager : PackageManager
{
    /// List of all SCM packages that can be fetched by this instance
    protected Package[Repository] scm;

    this()
    {
        NativePath pkg = NativePath("/tmp/dub-testsuite-nonexistant/packages/");
        NativePath user = NativePath("/tmp/dub-testsuite-nonexistant/user/");
        NativePath system = NativePath("/tmp/dub-testsuite-nonexistant/system/");
        super(pkg, user, system, false);
    }

    /// Disabled as semantic are not implementable unless a virtual FS is created
	public override @property void customCachePaths(NativePath[] custom_cache_paths)
    {
        assert(0, "Function not implemented");
    }

    /// Ditto
    public override Package store(NativePath src, PlacementLocation dest, string name, Version vers)
    {
        assert(0, "Function not implemented");
    }

    /**
     * This function usually scans the filesystem for packages.
     *
     * We don't want to do IO access and rely on users adding the packages
     * before the test starts instead.
     *
     * Note: Deprecated `refresh(bool)` does IO, but it's deprecated
     */
    public override void refresh()
    {
        // Do nothing
    }

    /**
     * Looks up a specific package
     *
     * Unlike its parent class, no lazy loading is performed.
     * Additionally, as they are already deprecated, overrides are
     * disabled and not available.
     */
	public override Package getPackage(string name, Version vers, bool enable_overrides = false)
    {
        //assert(!enable_overrides, "Overrides are not implemented for TestPackageManager");

        // Implementation inspired from `PackageManager.lookup`,
        // except we replaced `load` with `lookup`.
        if (auto pkg = this.m_internal.lookup(name, vers, this))
			return pkg;

		foreach (ref location; this.m_repositories)
			if (auto p = location.lookup(name, vers, this))
				return p;

		return null;
    }

	/**
	 * Re-Implementation of `loadSCMPackage`.
	 *
	 * The base implementation will do a `git` clone, which we would like to avoid.
	 * Instead, we allow unittests to explicitly define what packages should be
	 * reachable in a given test.
	 */
	public override Package loadSCMPackage(string name, Repository repo)
	{
        import std.string : chompPrefix;

		// We're trying to match `loadGitPackage` as much as possible
		if (!repo.ref_.startsWith("~") && !repo.ref_.isGitHash)
			return null;

		string gitReference = repo.ref_.chompPrefix("~");
		NativePath destination = this.getPackagePath(PlacementLocation.user, name, repo.ref_);
		destination ~= name;
		destination.endsWithSlash = true;

		foreach (p; getPackageIterator(name))
			if (p.path == destination)
				return p;

		return this.loadSCMRepository(name, repo);
	}

	/// The private part of `loadSCMPackage`
	protected Package loadSCMRepository(string name, Repository repo)
	{
		if (auto prepo = repo in this.scm) {
            this.add(*prepo);
            return *prepo;
        }
		return null;
	}

    /**
     * Adds a `Package` to this `PackageManager`
     *
     * This is currently only available in unittests as it is a convenience
     * function used by `TestDub`, but could be generalized once IO has been
     * abstracted away from this class.
     */
	public Package add(Package pkg)
	{
		// See `PackageManager.addPackages` for inspiration.
		assert(!pkg.subPackages.length, "Subpackages are not yet supported");
		this.m_internal.fromPath ~= pkg;
		return pkg;
	}

    /// Add a reachable SCM package to this `PackageManager`
    public void addTestSCMPackage(Repository repo, Package pkg)
    {
        this.scm[repo] = pkg;
    }
}

/**
 * Implements a `PackageSupplier` that doesn't do any IO
 *
 * This `PackageSupplier` needs to be pre-loaded with `Package` it can
 * find during the setup phase of the unittest.
 */
public class MockPackageSupplier : PackageSupplier
{
    /// Mapping of package name to packages, ordered by `Version`
    protected Package[][string] pkgs;

    /// URL this was instantiated with
    protected string url;

    ///
    public this(string url)
    {
        this.url = url;
    }

    ///
    public override @property string description()
    {
        return "unittest PackageSupplier for: " ~ this.url;
    }

    ///
    public override Version[] getVersions(string package_id)
    {
        if (auto ppkgs = package_id in this.pkgs)
            return (*ppkgs).map!(pkg => pkg.version_).array;
        return null;
    }

    ///
    public override void fetchPackage(
        NativePath path, string package_id, in VersionRange dep, bool pre_release)
    {
        assert(0, this.url ~ " - fetchPackage not implemented for: " ~ package_id);
    }

    ///
    public override Json fetchPackageRecipe(
        string package_id, in VersionRange dep, bool pre_release)
    {
        import dub.recipe.json;

        if (auto ppkgs = package_id in this.pkgs)
            foreach_reverse (pkg; *ppkgs)
                if ((!pkg.version_.isPreRelease || pre_release) &&
                    dep.matches(pkg.version_))
                    return toJson(pkg.recipe);
        return Json.init;
    }

    ///
    public override SearchResult[] searchPackages(string query)
    {
        assert(0, this.url ~ " - searchPackages not implemented for: " ~ query);
    }
}
