#!/usr/bin/env python3
import argparse
import os
import subprocess
from pathlib import Path

from common import (
    get_testcases,
    is_failure,
    is_passing_skipped_test,
    is_unexpected_success,
    key,
    open_test_results,
)
from download_reports import download_reports


"""
Usage: update_failures.py /path/to/dynamo_test_failures.py /path/to/test commit_sha

Best-effort updates the xfail and skip files under test directory
by parsing test reports.

You'll need to provide the commit_sha for the latest commit on a PR
from which we will pull CI test results.

Instructions:
- On your PR, add the "keep-going" label to ensure that all the tests are
  failing (as opposed to CI stopping on the first failure). You may need to
  restart your test jobs by force-pushing to your branch for CI to pick
  up the "keep-going" label.
- Wait for all the tests to finish running.
- Find the full SHA of your commit and run this command.

This script requires the `gh` cli. You'll need to install it and then
authenticate with it via `gh auth login` before using this script.
https://docs.github.com/en/github-cli/github-cli/quickstart
"""


def patch_file(
    filename, test_dir, unexpected_successes, new_xfails, new_skips, unexpected_skips
):
    failures_directory = os.path.join(test_dir, "dynamo_expected_failures")
    skips_directory = os.path.join(test_dir, "dynamo_skips")

    dynamo_expected_failures = set(os.listdir(failures_directory))
    dynamo_skips = set(os.listdir(skips_directory))

    # These are hand written skips
    extra_dynamo_skips = set()
    with open(filename) as f:
        start = False
        for text in f.readlines():
            text = text.strip()
            if start:
                if text == "}":
                    break
                extra_dynamo_skips.add(text.strip(',"'))
            else:
                if text == "extra_dynamo_skips = {":
                    start = True

    def format(testcase):
        classname = testcase.attrib["classname"]
        name = testcase.attrib["name"]
        return f"{classname}.{name}"

    formatted_unexpected_successes = {
        f"{format(test)}" for test in unexpected_successes.values()
    }
    formatted_unexpected_skips = {
        f"{format(test)}" for test in unexpected_skips.values()
    }
    formatted_new_xfails = [f"{format(test)}" for test in new_xfails.values()]
    formatted_new_skips = [f"{format(test)}" for test in new_skips.values()]

    def remove_file(path, name):
        file = os.path.join(path, name)
        cmd = ["git", "rm", file]
        subprocess.run(cmd)

    def add_file(path, name):
        file = os.path.join(path, name)
        with open(file, "w") as fp:
            pass
        cmd = ["git", "add", file]
        subprocess.run(cmd)

    covered_unexpected_successes = set()

    # dynamo_expected_failures
    for test in dynamo_expected_failures:
        if test in formatted_unexpected_successes:
            covered_unexpected_successes.add(test)
            remove_file(failures_directory, test)
    for test in formatted_new_xfails:
        add_file(failures_directory, test)

    leftover_unexpected_successes = (
        formatted_unexpected_successes - covered_unexpected_successes
    )
    if len(leftover_unexpected_successes) > 0:
        print(
            "WARNING: we were unable to remove these "
            f"{len(leftover_unexpected_successes)} expectedFailures:"
        )
        for stuff in leftover_unexpected_successes:
            print(stuff)

    # dynamo_skips
    for test in dynamo_skips:
        if test in formatted_unexpected_skips:
            remove_file(skips_directory, test)
    for test in extra_dynamo_skips:
        if test in formatted_unexpected_skips:
            print(
                f"WARNING: {test} in dynamo_test_failures.py needs to be removed manually"
            )
    for test in formatted_new_skips:
        add_file(skips_directory, test)


def get_intersection_and_outside(a_dict, b_dict):
    a = set(a_dict.keys())
    b = set(b_dict.keys())
    intersection = a.intersection(b)
    outside = (a.union(b)) - intersection

    def build_dict(keys):
        result = {}
        for k in keys:
            if k in a_dict:
                result[k] = a_dict[k]
            else:
                result[k] = b_dict[k]
        return result

    return build_dict(intersection), build_dict(outside)


def update(filename, test_dir, py38_dir, py311_dir, also_remove_skips):
    def read_test_results(directory):
        xmls = open_test_results(directory)
        testcases = get_testcases(xmls)
        unexpected_successes = {
            key(test): test for test in testcases if is_unexpected_success(test)
        }
        failures = {key(test): test for test in testcases if is_failure(test)}
        passing_skipped_tests = {
            key(test): test for test in testcases if is_passing_skipped_test(test)
        }
        return unexpected_successes, failures, passing_skipped_tests

    (
        py38_unexpected_successes,
        py38_failures,
        py38_passing_skipped_tests,
    ) = read_test_results(py38_dir)
    (
        py311_unexpected_successes,
        py311_failures,
        py311_passing_skipped_tests,
    ) = read_test_results(py311_dir)

    unexpected_successes = {**py38_unexpected_successes, **py311_unexpected_successes}
    _, skips = get_intersection_and_outside(
        py38_unexpected_successes, py311_unexpected_successes
    )
    xfails, more_skips = get_intersection_and_outside(py38_failures, py311_failures)
    if also_remove_skips:
        unexpected_skips, _ = get_intersection_and_outside(
            py38_passing_skipped_tests, py311_passing_skipped_tests
        )
    else:
        unexpected_skips = {}
    all_skips = {**skips, **more_skips}
    print(
        f"Discovered {len(unexpected_successes)} new unexpected successes, "
        f"{len(xfails)} new xfails, {len(all_skips)} new skips, {len(unexpected_skips)} new unexpected skips"
    )
    return patch_file(
        filename, test_dir, unexpected_successes, xfails, all_skips, unexpected_skips
    )


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        prog="update_dynamo_test_failures",
        description="Read from logs and update the dynamo_test_failures file",
    )
    # dynamo_test_failures path
    parser.add_argument(
        "filename",
        nargs="?",
        default=str(
            Path(__file__).absolute().parents[2]
            / "torch/testing/_internal/dynamo_test_failures.py"
        ),
        help="Optional path to dynamo_test_failures.py",
    )
    # test path
    parser.add_argument(
        "test_dir",
        nargs="?",
        default=str(Path(__file__).absolute().parents[2] / "test"),
        help="Optional path to test folder",
    )
    parser.add_argument(
        "commit",
        help=(
            "The commit sha for the latest commit on a PR from which we will "
            "pull CI test results, e.g. 7e5f597aeeba30c390c05f7d316829b3798064a5"
        ),
    )
    parser.add_argument(
        "--also-remove-skips",
        help="Also attempt to remove skips. WARNING: does not guard against test flakiness",
        action="store_true",
    )
    args = parser.parse_args()
    assert Path(args.filename).exists(), args.filename
    assert Path(args.test_dir).exists(), args.test_dir
    dynamo39, dynamo311 = download_reports(args.commit, ("dynamo39", "dynamo311"))
    update(args.filename, args.test_dir, dynamo39, dynamo311, args.also_remove_skips)
