"""
Helper methods for running the functional smoke tests.
"""

import os
import re
import json
import logging

from operator import itemgetter
from pathlib import Path

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions
from scubagoggles.orchestrator import Orchestrator
from scubagoggles.config import UserConfig
from scubagoggles.user_setup import default_file_names
from scubagoggles.version import Version

OUTPUT_DIRECTORY = 'GWSBaselineConformance'
BASELINE_REPORT_H1 = 'SCuBA GWS Secure Configuration Baseline Reports'
CISA_GOV_URL = 'https://www.cisa.gov/scuba'
SCUBAGOGGLES_BASELINES_URL = 'https://github.com/cisagov/ScubaGoggles/tree/main/baselines'
RESULTS_RE = re.compile(r"ScubaResults_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}\.json")

log = logging.getLogger(__name__)


def get_output_path() -> Path:
    """
    Get the latest output directory created by `scubagoggles gws`.
    The default name is "GWSBaselineConformance_<timestamp>.

    Returns:
        str: The path to the latest output directory
    """

    config = UserConfig()
    output_dir = config.output_dir
    dir_pattern = f'{default_file_names.output_folder_name}*'

    directories = [(d, d.stat().st_ctime)
                   for d in output_dir.glob(dir_pattern)]
    directories.sort(key = itemgetter(1), reverse = True)

    return directories[0][0]


def prepend_file_protocol(path: str) -> str:
    """
    Prepends "file://", which is used to locate files on a local filesystem.

    Returns:
        str: Path to a file with the local filesystem prepended
    """
    if not path.startswith('file://'):
        path = 'file://' + path
    return path


def verify_output_type(output_path: Path, output: list) -> list:
    """
    Checks if the output generated from `scubagoggles` creates the correct output.
    Validate files/directories and catch invalid json.

    Args:
        output_path: The output path, i.e. "GWSBaselineConformance_<timestamp>"
        output: Initialized as an empty list

    Returns:
        list: All output file and directory names
    """
    for entry in output_path.iterdir():
        output.append(entry)

        # Check if entry is a valid directory or file
        # If a valid directory, then recurse
        if entry.is_dir():
            assert True
            verify_output_type(entry, output)
        elif os.path.isfile(entry):

            # Check for valid json
            if entry.suffix.lower() == '.json':
                try:
                    with entry.open(encoding = 'utf-8') as jsonfile:
                        json.load(jsonfile)
                except ValueError as e:
                    raise ValueError(f'{entry} contains invalid json') from e
            assert True
        else:
            raise OSError('Entry is not a directory or file (symlink, etc.)')
    return output


def sample_report_dir() -> Path:

    """Returns the location of the ScubaGoggles sample report directory.  It's
    assumed that this directory located up the directory hierarchy where this
    script is located.

    :return: Path pointing to the sample report directory.
    """

    current_dir = Path(__file__).parent
    while current_dir.anchor != str(current_dir):

        report_path = current_dir / 'sample-report'
        if report_path.is_dir():
            return report_path

        current_dir = current_dir.parent

    raise NotADirectoryError('? sample report directory missing')


def top_report_url(output_path: Path) -> str:

    """Returns the URL for the top-level HTML ScubaGoggles baseline report,
    given the output directory from a conformance run.

    :param Path output_path: ScubaGoggles output directory
    :return: local (file://) URL for the top-level HTML report.
    """

    report_path = output_path / f'{default_file_names.report_output_name}.html'

    return prepend_file_protocol(str(report_path))


def get_required_entries(sample_report, required_entries) -> list:
    """
    From the "sample-report" directory, add all file and directory names
    into a list "required_entries". All entries must be present
    for smoke tests to pass.

    Args:
        sample_report: Path where "sample-report" is located in the project
        required_entries: Initialized as an empty list

    Returns:
        list: All required file and directory names
    """
    for entry in sample_report.iterdir():
        required_entries.append(entry.name)
        if entry.is_dir():
            get_required_entries(entry, required_entries)

    return required_entries


def verify_all_outputs_exist(output: list, required_entries: list):
    """
    Verify all files and directories are created after running `scubagoggles gws`.

    Args:
        output: a list of all files and directories generated by `scubagoggles gws`
        required_entries: a list of all required file and directory names
    """
    output_names = {entry.name for entry in output}
    for required_entry in required_entries:
        if RESULTS_RE.match(required_entry):
            for output_name in output_names:
                if RESULTS_RE.match(output_name):
                    break
            else:
                raise ValueError(f'{required_entry} was not found in the generated report')
        elif required_entry in output_names:
            continue
        else:
            raise ValueError(f'{required_entry} was not found in the generated report')


def verify_scubaresults(output_path: Path):
    """
    Verify "ScubaResults.json" is valid, and check if any errors
    are displayed in the reports.

    Args:
        output_path: ScubaGoggles output directory containing the results JSON
            file.
    """
    for fname in os.listdir(output_path):
        if RESULTS_RE.match(fname):
            break
    else:
        raise ValueError('ScubaResults*.json file was not found in the generated report')
    results_path = output_path / fname
    with results_path.open(encoding = 'utf-8') as jsonfile:
        scubaresults = json.load(jsonfile)

    summaries = scubaresults['Summary']
    for product, summary in summaries.items():
        if summary['Errors'] != 0:
            raise ValueError(f'{product} contains errors in the report')


def run_selenium(browser, customerdomain):
    """
    Run Selenium tests against the generated reports.

    Args:
        browser: A Selenium WebDriver instance
        customerdomain: The customer domain
    """
    verify_navigation_links(browser)
    h1 = browser.find_element(By.TAG_NAME, 'h1').text
    assert h1 == BASELINE_REPORT_H1

    gws_products = Orchestrator.gws_products()
    products = {
        product: { 'title': f'{product} Baseline Report' }
        for product in gws_products['prod_to_fullname'].values()
    }

    # Before entering loop check that we actually display 9 rows in table
    reports_table = get_reports_table(browser)

    if len(reports_table) == 9:
        for i in range(len(reports_table)):

            # Check if customerdomain is present in agency table
            # Skip tool version if assessing the parent report
            verify_tenant_table(browser, customerdomain, True)

            reports_table = get_reports_table(browser)[i]
            baseline_report = reports_table.find_elements(By.TAG_NAME, 'td')[0]
            product = baseline_report.text
            assert product in products

            individual_report_anchor = baseline_report.find_element(By.TAG_NAME, 'a')
            individual_report_anchor_href = individual_report_anchor.get_attribute('href')
            individual_report_anchor.click()
            current_url = browser.current_url()
            assert individual_report_anchor_href == current_url

            # Check at the individual report level
            verify_navigation_links(browser)
            h1 = browser.find_element(By.TAG_NAME, 'h1').text
            assert h1 == products[product]['title']

            # Check if customerdomain and tool version are present in individual report
            verify_tenant_table(browser, customerdomain, False)

            policy_tables = browser.find_elements(By.TAG_NAME, 'table')
            for table in policy_tables[1:]:

                # Verify policy table headers are correct
                headers = (
                    table.find_element(By.TAG_NAME, 'thead')
                    .find_elements(By.TAG_NAME, 'tr')[0]
                    .find_elements(By.TAG_NAME, 'th')
                )
                if len(headers) == 3:
                    # Is this the rules table?
                    assert headers[0].text == 'Alert Name'
                    assert headers[1].text == 'Description'
                    assert headers[2].text == 'Status'
                else:
                    # If not, this has to be a generic result table
                    assert len(headers) == 5
                    assert headers[0].text == 'Control ID'
                    assert headers[1].text == 'Requirement'
                    assert headers[2].text == 'Result'
                    assert headers[3].text == 'Criticality'
                    assert headers[4].text == 'Details'

                # Verify policy table rows are populated
                tbody = table.find_element(By.TAG_NAME, 'tbody')
                rows = tbody.find_elements(By.TAG_NAME, 'tr')
                assert len(rows) > 0

            parent_report_anchor = (
                browser.find_element(By.TAG_NAME, 'header')
                .find_element(By.TAG_NAME, 'a')
            )
            parent_report_anchor_href = parent_report_anchor.get_attribute('href')
            parent_report_anchor.click()
            current_url = browser.current_url()
            assert parent_report_anchor_href == current_url

            WebDriverWait(browser, 10).until(
                expected_conditions.presence_of_element_located(
                    (By.TAG_NAME, 'body')
                )
            )
    else:
        raise ValueError('Expected the reports table to have a length of 9')


def verify_navigation_links(browser):
    """
    For each baseline report, check that the navigation links display correctly.

    Args:
        browser: A Selenium WebDriver instance
    """
    links = (
        browser.find_element(By.CLASS_NAME, 'links')
        .find_elements(By.TAG_NAME, 'a')
    )
    if len(links) == 2:
        assert links[0].get_attribute('href') == CISA_GOV_URL
        assert links[1].get_attribute('href') == SCUBAGOGGLES_BASELINES_URL


def get_reports_table(browser):
    """
    Get the reports table element from the DOM.
    (Table in BaselineReports.html with list of baselines and pass/fail/warning of each)

    Args:
        browser: A Selenium WebDriver instance
    """
    return (
        browser.find_elements(By.TAG_NAME, 'table')[1]
        .find_element(By.TAG_NAME, 'tbody')
        .find_elements(By.TAG_NAME, 'tr')
    )

def verify_tenant_table(browser, customerdomain, parent):
    """
    Get the tenant table rows elements from the DOM.
    (Table at the top of each report with customer domain, baseline/tool version)

    Args:
        browser: A Selenium WebDriver instance
        customerdomain: The customer domain
        parent: boolean to determine parent/individual reports
    """
    tenant_table_rows = (
        browser.find_element(By.TAG_NAME, 'table')
        .find_element(By.TAG_NAME, 'tbody')
        .find_elements(By.TAG_NAME, 'tr')
    )
    assert len(tenant_table_rows) == 2

    # The tenant table has 2 rows - the heading and the data.  First, create
    # a map of the headings to their corresponding row indices.  This will
    # allow us to get the values by heading.

    error_occurred = False

    row0, row1 = tenant_table_rows
    heading_index_map = {e.text.strip(): i for i, e
                         in enumerate(row0.find_elements(By.TAG_NAME, 'th'))}

    # These are the checks to be done.  Always check the customer domain, and
    # for the individual reports check that the tool version reported is
    # correct.

    tests = [('Customer Domain', customerdomain)]

    if not parent:
        tests.append(('Tool Version', Version.current))

    values = [e.text for e in row1.find_elements(By.TAG_NAME, 'td')]

    for heading, expected_value in tests:

        if heading not in heading_index_map:
            log.error('"%s" - heading missing from tenant table', heading)
            error_occurred = True
            continue

        index = heading_index_map[heading]

        if index >= len(values):
            log.error('value missing for "%s" in tenant table', heading)
            error_occurred = True
            continue

        value = values[index]

        if value != expected_value:
            log.error('"%s" != "%s" (expected) for "%s"',
                      value,
                      expected_value,
                      heading)
            error_occurred = True

    assert not error_occurred
