"""Utilities for testing bazel."""
#
# Copyright 2015 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

load("@rules_shell//shell:sh_test.bzl", "sh_test")
load(":test_rules_private.bzl", "BASH_RUNFILES_DEP", "INIT_BASH_RUNFILES")

_SH_STUB = "\n".join(["#!/bin/bash"] + INIT_BASH_RUNFILES + [
    "function add_ws_name() {",
    '  [[ "$1" =~ external/* ]] && echo "${1#external/}" || echo "$TEST_WORKSPACE/$1"',
    "}",
    "",
])

def _bash_rlocation(f):
    return '"$(rlocation "$(add_ws_name "%s")")"' % f.short_path

def _make_sh_test(name, **kwargs):
    sh_test(
        name = name,
        srcs = [name + "_impl"],
        data = [name + "_impl"],
        deps = [BASH_RUNFILES_DEP],
        **kwargs
    )

_TEST_ATTRS = {
    "args": None,
    "size": None,
    "timeout": None,
    "flaky": None,
    "local": None,
    "shard_count": None,
}

def _helper_rule_attrs(test_attrs, own_attrs):
    r = {}
    r.update({k: v for k, v in test_attrs.items() if k not in _TEST_ATTRS})
    r.update(own_attrs)
    r.update(
        dict(
            testonly = 1,
            visibility = ["//visibility:private"],
        ),
    )
    return r

### First, trivial tests that either always pass, always fail,
### or sometimes pass depending on a trivial computation.

def success_target(ctx, msg, exe = None):
    """Return a success for an analysis test.

    The test rule must have an executable output.

    Args:
      ctx: the Bazel rule context
      msg: an informative message to display
      exe: the output artifact (must have been created with
           ctx.actions.declare_file or declared in ctx.output), or None meaning
           ctx.outputs.executable

    Returns:
      DefaultInfo that can be added to a sh_test's srcs AND data. The test will
      always pass.
    """
    exe = exe or ctx.outputs.executable
    ctx.actions.write(
        output = exe,
        content = "#!/bin/bash\ncat <<'__eof__'\n" + msg + "\n__eof__\necho",
        is_executable = True,
    )
    return [DefaultInfo(files = depset([exe]))]

def _successful_test_impl(ctx):
    return success_target(ctx, ctx.attr.msg, exe = ctx.outputs.out)

_successful_rule = rule(
    attrs = {
        "msg": attr.string(mandatory = True),
        "out": attr.output(),
    },
    implementation = _successful_test_impl,
)

def successful_test(name, msg, **kwargs):
    _successful_rule(
        **_helper_rule_attrs(
            kwargs,
            dict(
                name = name + "_impl",
                msg = msg,
                out = name + "_impl.sh",
            ),
        )
    )

    _make_sh_test(name, **kwargs)

def failure_target(ctx, msg, exe = None):
    """Return a failure for an analysis test.

    Args:
      ctx: the Bazel rule context
      msg: an informative message to display
      exe: the output artifact (must have been created with
           ctx.actions.declare_file or declared in ctx.output), or None meaning
           ctx.outputs.executable

    Returns:
      DefaultInfo that can be added to a sh_test's srcs AND data. The test will
      always fail.
    """

    ### fail(msg) ### <--- This would fail at analysis time.
    exe = exe or ctx.outputs.executable
    ctx.actions.write(
        output = exe,
        content = "#!/bin/bash\ncat >&2 <<'__eof__'\n" + msg + "\n__eof__\nexit 1",
        is_executable = True,
    )
    return [DefaultInfo(files = depset([exe]))]

def _failed_test_impl(ctx):
    return failure_target(ctx, ctx.attr.msg, exe = ctx.outputs.out)

_failed_rule = rule(
    attrs = {
        "msg": attr.string(mandatory = True),
        "out": attr.output(),
    },
    implementation = _failed_test_impl,
)

def failed_test(name, msg, **kwargs):
    _failed_rule(
        **_helper_rule_attrs(
            kwargs,
            dict(
                name = name + "_impl",
                msg = msg,
                out = name + "_impl.sh",
            ),
        )
    )

    _make_sh_test(name, **kwargs)

### Second, general purpose utilities

def assert_(condition, string = "assertion failed", *args):
    """Trivial assertion mechanism.

    Args:
      condition: a generalized boolean expected to be true
      string: a format string for the error message should the assertion fail
      *args: format arguments for the error message should the assertion fail

    Returns:
      None.

    Raises:
      an error if the condition isn't true.
    """

    if not condition:
        fail(string % args)

def strip_prefix(prefix, string):
    assert_(
        string.startswith(prefix),
        "%s does not start with %s",
        string,
        prefix,
    )
    return string[len(prefix):len(string)]

def expectation_description(expect = None, expect_failure = None):
    """Turn expectation of result or error into a string."""
    if expect_failure:
        return "failure " + str(expect_failure)
    else:
        return "result " + repr(expect)

def check_results(result, failure, expect, expect_failure):
    """See if actual computation results match expectations.

    Args:
      result: the result returned by the test if it ran to completion
      failure: the failure message caught while testing, if any
      expect: the expected result for a successful test, if no failure expected
      expect_failure: the expected failure message for the test, if any

    Returns:
      a pair (tuple) of a boolean (true if success) and a message (string).
    """
    wanted = expectation_description(expect, expect_failure)
    found = expectation_description(result, failure)
    if wanted == found:
        return (True, "successfully computed " + wanted)
    else:
        return (False, "expect " + wanted + " but found " + found)

def load_results(
        name,
        result = None,
        failure = None,
        expect = None,
        expect_failure = None):
    """issue load-time results of a test.

    Args:
      name: the name of the Bazel rule at load time.
      result: the result returned by the test if it ran to completion
      failure: the failure message caught while testing, if any
      expect: the expected result for a successful test, if no failure expected
      expect_failure: the expected failure message for the test, if any

    Returns:
      None, after issuing a rule that will succeed at execution time if
      expectations were met.
    """
    (is_success, msg) = check_results(result, failure, expect, expect_failure)
    this_test = successful_test if is_success else failed_test
    return this_test(name = name, msg = msg)

def analysis_results(
        ctx,
        result = None,
        failure = None,
        expect = None,
        expect_failure = None):
    """issue analysis-time results of a test.

    Args:
      ctx: the Bazel rule context
      result: the result returned by the test if it ran to completion
      failure: the failure message caught while testing, if any
      expect: the expected result for a successful test, if no failure expected
      expect_failure: the expected failure message for the test, if any

    Returns:
      DefaultInfo that can be added to a sh_test's srcs AND data. The test will
      always succeed at execution time if expectation were met,
      or fail at execution time if they didn't.
    """
    (is_success, msg) = check_results(result, failure, expect, expect_failure)
    this_test = success_target if is_success else failure_target
    return this_test(ctx, msg)

### Simple tests

def _rule_test_rule_impl(ctx):
    """check that a rule generates the desired outputs and providers."""
    rule_ = ctx.attr.rule
    rule_name = str(rule_.label)
    exe = ctx.outputs.out
    if ctx.attr.generates:
        # Generate the proper prefix to remove from generated files.
        prefix_parts = []

        if rule_.label.workspace_root:
            # Create a prefix that is correctly relative to the output of this rule.
            prefix_parts = ["..", strip_prefix("external/", rule_.label.workspace_root)]

        if rule_.label.package:
            prefix_parts.append(rule_.label.package)

        prefix = "/".join(prefix_parts)

        if prefix:
            # If the prefix isn't empty, it needs a trailing slash.
            prefix = prefix + "/"

        # TODO(bazel-team): Use set() instead of sorted() once
        # set comparison is implemented.
        # TODO(bazel-team): Use a better way to determine if two paths refer to
        # the same file.
        generates = sorted(ctx.attr.generates)
        generated = sorted([
            strip_prefix(prefix, f.short_path)
            for f in rule_.files.to_list()
        ])
        if generates != generated:
            fail("rule %s generates %s not %s" %
                 (rule_name, repr(generated), repr(generates)))
    provides = ctx.attr.provides
    if provides:
        files = []
        commands = []
        for k in provides.keys():
            if hasattr(rule_, k):
                v = repr(getattr(rule_, k))
            else:
                fail(("rule %s doesn't provide attribute %s. " +
                      "Its list of attributes is: %s") %
                     (rule_name, k, dir(rule_)))
            file_ = ctx.actions.declare_file(exe.basename + "." + k)
            files += [file_]
            regexp = provides[k]
            commands += [
                "file_=%s" % _bash_rlocation(file_),
                "if ! grep %s \"$file_\" ; then echo 'bad %s:' ; cat \"$file_\" ; echo ; exit 1 ; fi" %
                (repr(regexp), k),
            ]
            ctx.actions.write(output = file_, content = v)
        script = _SH_STUB + "\n".join(commands)
        ctx.actions.write(output = exe, content = script, is_executable = True)
        return [DefaultInfo(files = depset([exe]), runfiles = ctx.runfiles([exe] + files))]
    else:
        return success_target(ctx, "success", exe = exe)

_rule_test_rule = rule(
    attrs = {
        "rule": attr.label(mandatory = True),
        "generates": attr.string_list(),
        "provides": attr.string_dict(),
        "out": attr.output(),
    },
    implementation = _rule_test_rule_impl,
)

def rule_test(name, rule, generates = None, provides = None, **kwargs):
    _rule_test_rule(
        **_helper_rule_attrs(
            kwargs,
            dict(
                name = name + "_impl",
                rule = rule,
                generates = generates,
                provides = provides,
                out = name + ".sh",
            ),
        )
    )

    _make_sh_test(name, **kwargs)

def _file_test_rule_impl(ctx):
    """check that a file has a given content."""
    exe = ctx.outputs.out
    file_ = ctx.file.file
    content = ctx.attr.content
    regexp = ctx.attr.regexp
    matches = ctx.attr.matches
    if bool(content) == bool(regexp):
        fail("Must specify one and only one of content or regexp")
    if content and matches != -1:
        fail("matches only makes sense with regexp")
    if content:
        dat = ctx.actions.declare_file(exe.basename + ".dat")
        ctx.actions.write(
            output = dat,
            content = content,
        )
        script = "diff -u %s %s" % (_bash_rlocation(dat), _bash_rlocation(file_))
        ctx.actions.write(
            output = exe,
            content = _SH_STUB + script,
            is_executable = True,
        )
        return [DefaultInfo(files = depset([exe]), runfiles = ctx.runfiles([exe, dat, file_]))]
    if matches != -1:
        script = "[ %s == $(grep -c %s %s) ]" % (
            matches,
            repr(regexp),
            _bash_rlocation(file_),
        )
    else:
        script = "grep %s %s" % (repr(regexp), _bash_rlocation(file_))
    ctx.actions.write(
        output = exe,
        content = _SH_STUB + script,
        is_executable = True,
    )
    return [DefaultInfo(files = depset([exe]), runfiles = ctx.runfiles([exe, file_]))]

_file_test_rule = rule(
    attrs = {
        "file": attr.label(
            mandatory = True,
            allow_single_file = True,
        ),
        "content": attr.string(default = ""),
        "regexp": attr.string(default = ""),
        "matches": attr.int(default = -1),
        "out": attr.output(),
    },
    implementation = _file_test_rule_impl,
)

def file_test(name, file, content = None, regexp = None, matches = None, **kwargs):
    _file_test_rule(
        **_helper_rule_attrs(
            kwargs,
            dict(
                name = name + "_impl",
                file = file,
                content = content or "",
                regexp = regexp or "",
                matches = matches if (matches != None) else -1,
                out = name + "_impl.sh",
            ),
        )
    )
    _make_sh_test(name, **kwargs)
