# Copyright Google Inc. All Rights Reserved.
#
# Use of this source code is governed by an MIT-style license that can be
# found in the LICENSE file at https://angular.io/license
"Run end-to-end tests with Protractor"

load(
    "@build_bazel_rules_nodejs//internal:node.bzl",
    "expand_path_into_runfiles",
    "sources_aspect",
)
load("@io_bazel_rules_webtesting//web:web.bzl", "web_test_suite")
load("@io_bazel_rules_webtesting//web/internal:constants.bzl", "DEFAULT_WRAPPED_TEST_TAGS")
load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary")

_CONF_TMPL = "//packages/bazel/src/protractor:protractor.conf.js"

def _short_path_to_manifest_path(ctx, short_path):
    if short_path.startswith("../"):
        return short_path[3:]
    else:
        return ctx.workspace_name + "/" + short_path

def _protractor_web_test_impl(ctx):
    configuration = ctx.actions.declare_file(
        "%s.conf.js" % ctx.label.name,
        sibling = ctx.outputs.executable,
    )

    files = depset(ctx.files.srcs)
    for d in ctx.attr.deps:
        if hasattr(d, "node_sources"):
            files = depset(transitive = [files, d.node_sources])
        elif hasattr(d, "files"):
            files = depset(transitive = [files, d.files])

    specs = [
        expand_path_into_runfiles(ctx, f.short_path)
        for f in files.to_list()
    ]

    configuration_sources = []
    if ctx.file.configuration:
        configuration_sources = [ctx.file.configuration]
    if hasattr(ctx.attr.configuration, "node_sources"):
        configuration_sources = ctx.attr.configuration.node_sources.to_list()

    configuration_file = ctx.file.configuration
    if hasattr(ctx.attr.configuration, "typescript"):
        configuration_file = ctx.attr.configuration.typescript.es5_sources.to_list()[0]

    on_prepare_sources = []
    if ctx.file.on_prepare:
        on_prepare_sources = [ctx.file.on_prepare]
    if hasattr(ctx.attr.on_prepare, "node_sources"):
        on_prepare_sources = ctx.attr.on_prepare.node_sources.to_list()

    on_prepare_file = ctx.file.on_prepare
    if hasattr(ctx.attr.on_prepare, "typescript"):
        on_prepare_file = ctx.attr.on_prepare.typescript.es5_sources.to_list()[0]

    ctx.actions.expand_template(
        output = configuration,
        template = ctx.file._conf_tmpl,
        substitutions = {
            "TMPL_config": expand_path_into_runfiles(ctx, configuration_file.short_path) if configuration_file else "",
            "TMPL_on_prepare": expand_path_into_runfiles(ctx, on_prepare_file.short_path) if on_prepare_file else "",
            "TMPL_workspace": ctx.workspace_name,
            "TMPL_server": ctx.executable.server.short_path if ctx.executable.server else "",
            "TMPL_specs": "\n".join(["      '%s'," % e for e in specs]),
        },
    )

    runfiles = [configuration] + configuration_sources + on_prepare_sources

    ctx.actions.write(
        output = ctx.outputs.executable,
        is_executable = True,
        content = """#!/usr/bin/env bash
# Immediately exit if any command fails.
set -e

if [ -e "$RUNFILES_MANIFEST_FILE" ]; then
  while read line; do
    declare -a PARTS=($line)
    if [ "${{PARTS[0]}}" == "{TMPL_protractor}" ]; then
      readonly PROTRACTOR=${{PARTS[1]}}
    elif [ "${{PARTS[0]}}" == "{TMPL_conf}" ]; then
      readonly CONF=${{PARTS[1]}}
    fi
  done < $RUNFILES_MANIFEST_FILE
else
  readonly PROTRACTOR=../{TMPL_protractor}
  readonly CONF=../{TMPL_conf}
fi

export HOME=$(mktemp -d)

# Print the protractor version in the test log
PROTRACTOR_VERSION=$($PROTRACTOR --version)
echo "Protractor $PROTRACTOR_VERSION"

# Run the protractor binary
$PROTRACTOR $CONF
""".format(
            TMPL_protractor = _short_path_to_manifest_path(ctx, ctx.executable.protractor.short_path),
            TMPL_conf = _short_path_to_manifest_path(ctx, configuration.short_path),
        ),
    )
    return [DefaultInfo(
        files = depset([ctx.outputs.executable]),
        runfiles = ctx.runfiles(
            files = runfiles,
            transitive_files = files,
            # Propagate protractor_bin and its runfiles
            collect_data = True,
            collect_default = True,
        ),
        executable = ctx.outputs.executable,
    )]

_protractor_web_test = rule(
    implementation = _protractor_web_test_impl,
    test = True,
    executable = True,
    attrs = {
        "configuration": attr.label(
            doc = "Protractor configuration file",
            allow_single_file = True,
            aspects = [sources_aspect],
        ),
        "srcs": attr.label_list(
            doc = "A list of JavaScript test files",
            allow_files = [".js"],
        ),
        "on_prepare": attr.label(
            doc = """A file with a node.js script to run once before all tests run.
            If the script exports a function which returns a promise, protractor
            will wait for the promise to resolve before beginning tests.""",
            allow_single_file = True,
            aspects = [sources_aspect],
        ),
        "deps": attr.label_list(
            doc = "Other targets which produce JavaScript such as `ts_library`",
            allow_files = True,
            aspects = [sources_aspect],
        ),
        "data": attr.label_list(
            doc = "Runtime dependencies",
        ),
        "server": attr.label(
            doc = "Optional server executable target",
            executable = True,
            cfg = "target",
            single_file = False,
            allow_files = True,
        ),
        "protractor": attr.label(
            doc = "Protractor executable target (set by protractor_web_test macro)",
            executable = True,
            cfg = "target",
            single_file = False,
            allow_files = True,
        ),
        "_conf_tmpl": attr.label(
            default = Label(_CONF_TMPL),
            allow_single_file = True,
        ),
    },
)

def protractor_web_test(
        name,
        configuration = None,
        on_prepare = None,
        srcs = [],
        deps = [],
        data = [],
        server = None,
        tags = [],
        **kwargs):
    """Runs a protractor test in a browser.

    Args:
      name: The name of the test
      configuration: Protractor configuration file.
      on_prepare: A file with a node.js script to run once before all tests run.
          If the script exports a function which returns a promise, protractor
          will wait for the promise to resolve before beginning tests.
      srcs: JavaScript source files
      deps: Other targets which produce JavaScript such as `ts_library`
      data: Runtime dependencies
      server: Optional server executable target
      tags: Standard Bazel tags, this macro adds one for ibazel
      **kwargs: passed through to `_protractor_web_test`
    """

    protractor_bin_name = name + "_protractor_bin"

    nodejs_binary(
        name = protractor_bin_name,
        entry_point = "protractor/bin/protractor",
        data = srcs + deps + data,
        testonly = 1,
        visibility = ["//visibility:private"],
    )

    # Our binary dependency must be in data[] for collect_data to pick it up
    # FIXME: maybe we can just ask :protractor_bin_name for its runfiles attr
    web_test_data = data + [":" + protractor_bin_name]
    if server:
        web_test_data += [server]

    _protractor_web_test(
        name = name,
        configuration = configuration,
        on_prepare = on_prepare,
        srcs = srcs,
        deps = deps,
        data = web_test_data,
        server = server,
        protractor = protractor_bin_name,
        tags = tags + [
            # Users don't need to know that this tag is required to run under ibazel
            "ibazel_notify_changes",
        ],
        **kwargs
    )

def protractor_web_test_suite(
        name,
        configuration = None,
        on_prepare = None,
        srcs = [],
        deps = [],
        data = [],
        server = None,
        browsers = ["@io_bazel_rules_webtesting//browsers:chromium-local"],
        args = None,
        browser_overrides = None,
        config = None,
        flaky = None,
        local = None,
        shard_count = None,
        size = None,
        tags = [],
        test_suite_tags = None,
        timeout = None,
        visibility = None,
        web_test_data = [],
        wrapped_test_tags = None,
        **remaining_keyword_args):
    """Defines a test_suite of web_test targets that wrap a protractor_web_test target.

    Args:
      name: The base name of the test.
      configuration: Protractor configuration file.
      on_prepare: A file with a node.js script to run once before all tests run.
          If the script exports a function which returns a promise, protractor
          will wait for the promise to resolve before beginning tests.
      srcs: JavaScript source files
      deps: Other targets which produce JavaScript such as `ts_library`
      data: Runtime dependencies
      server: Optional server executable target
      browsers: A sequence of labels specifying the browsers to use.
      args: Args for web_test targets generated by this extension.
      browser_overrides: Dictionary; optional; default is an empty dictionary. A
        dictionary mapping from browser names to browser-specific web_test
        attributes, such as shard_count, flakiness, timeout, etc. For example:
        {'//browsers:chrome-native': {'shard_count': 3, 'flaky': 1}
         '//browsers:firefox-native': {'shard_count': 1, 'timeout': 100}}.
      config: Label; optional; Configuration of web test features.
      flaky: A boolean specifying that the test is flaky. If set, the test will
        be retried up to 3 times (default: 0)
      local: boolean; optional.
      shard_count: The number of test shards to use per browser. (default: 1)
      size: A string specifying the test size. (default: 'large')
      tags: A list of test tag strings to apply to each generated web_test target.
        This macro adds a couple for ibazel.
      test_suite_tags: A list of tag strings for the generated test_suite.
      timeout: A string specifying the test timeout (default: computed from size)
      visibility: List of labels; optional.
      web_test_data: Data dependencies for the web_test.
      wrapped_test_tags: A list of test tag strings to use for the wrapped test
      **remaining_keyword_args: Arguments for the wrapped test target.
    """

    # Check explicitly for None so that users can set this to the empty list
    if wrapped_test_tags == None:
        wrapped_test_tags = DEFAULT_WRAPPED_TEST_TAGS

    size = size or "large"

    wrapped_test_name = name + "_wrapped_test"
    protractor_bin_name = name + "_protractor_bin"

    # Users don't need to know that this tag is required to run under ibazel
    tags = tags + ["ibazel_notify_changes"]

    nodejs_binary(
        name = protractor_bin_name,
        entry_point = "protractor/bin/protractor",
        data = srcs + deps + data,
        testonly = 1,
        visibility = ["//visibility:private"],
    )

    # Our binary dependency must be in data[] for collect_data to pick it up
    # FIXME: maybe we can just ask the :protractor_bin_name for its runfiles attr
    web_test_data = web_test_data + [":" + protractor_bin_name]
    if server:
        web_test_data += [server]

    _protractor_web_test(
        name = wrapped_test_name,
        configuration = configuration,
        on_prepare = on_prepare,
        srcs = srcs,
        deps = deps,
        data = web_test_data,
        server = server,
        protractor = protractor_bin_name,
        args = args,
        flaky = flaky,
        local = local,
        shard_count = shard_count,
        size = size,
        tags = wrapped_test_tags,
        timeout = timeout,
        visibility = ["//visibility:private"],
        **remaining_keyword_args
    )

    web_test_suite(
        name = name,
        launcher = ":" + wrapped_test_name,
        args = args,
        browsers = browsers,
        browser_overrides = browser_overrides,
        config = config,
        data = web_test_data,
        flaky = flaky,
        local = local,
        shard_count = shard_count,
        size = size,
        tags = tags,
        test = wrapped_test_name,
        test_suite_tags = test_suite_tags,
        timeout = timeout,
        visibility = visibility,
    )
