import json
import sys
import traceback
from functools import partial
from pathlib import Path

import yaml

from cucu import config, init_scenario_hook_variables, logger
from cucu.config import CONFIG
from cucu.page_checks import init_page_checks
from cucu.utils import (
    TeeStream,
    ellipsize_filename,
    get_iso_timestamp_with_ms,
    parse_iso_timestamp,
    take_screenshot,
)

CONFIG.define(
    "SCENARIO_RESULTS_DIR",
    "the results directory for the currently executing scenario",
    default=None,
)
CONFIG.define(
    "SCENARIO_DOWNLOADS_DIR",
    "the browser downloads directory for the currently executing scenario",
    default=None,
)


init_page_checks()


def check_browser_initialized(ctx):
    """
    check browser session initialized otherwise throw an exception indicating
    such to be used consistently by all steps in this module.
    """

    if ctx.browser is None:
        raise RuntimeError("browser not currently open")


def before_all(ctx):
    CONFIG["__CUCU_CTX"] = ctx
    ctx.check_browser_initialized = partial(check_browser_initialized, ctx)
    ctx.worker_custom_data = {}

    CONFIG.snapshot("before_all")

    for hook in CONFIG["__CUCU_BEFORE_ALL_HOOKS"]:
        hook(ctx)


def after_all(ctx):
    # run the after all hooks
    for hook in CONFIG["__CUCU_AFTER_ALL_HOOKS"]:
        hook(ctx)

    CONFIG.restore(with_pop=True)


def before_feature(ctx, feature):
    if config.CONFIG["CUCU_RESULTS_DIR"] is not None:
        results_dir = Path(config.CONFIG["CUCU_RESULTS_DIR"])
        ctx.feature_dir = results_dir / ellipsize_filename(feature.name)


def after_feature(ctx, feature):
    pass


def before_scenario(ctx, scenario):
    # we want every scenario to start with the exact same reinitialized config
    # values and not bleed values between scenario runs
    CONFIG.restore()

    # we should load any cucurc.yml files in the path to the feature file
    # we are about to run so that the config values set for this feature are
    # correctly loaded.
    CONFIG.load_cucurc_files(ctx.feature.filename)

    init_scenario_hook_variables()

    ctx.scenario = scenario
    ctx.step_index = 0
    ctx.browsers = []
    ctx.browser = None
    ctx.section_step_stack = []

    # reset the step timer dictionary
    ctx.step_timers = {}
    scenario.start_at = get_iso_timestamp_with_ms()

    if config.CONFIG["CUCU_RESULTS_DIR"] is not None:
        ctx.scenario_dir = ctx.feature_dir / ellipsize_filename(scenario.name)
        CONFIG["SCENARIO_RESULTS_DIR"] = ctx.scenario_dir
        ctx.scenario_dir.mkdir(parents=True, exist_ok=True)

        ctx.scenario_downloads_dir = ctx.scenario_dir / "downloads"
        CONFIG["SCENARIO_DOWNLOADS_DIR"] = ctx.scenario_downloads_dir
        ctx.scenario_downloads_dir.mkdir(parents=True, exist_ok=True)

        ctx.scenario_logs_dir = ctx.scenario_dir / "logs"
        CONFIG["SCENARIO_LOGS_DIR"] = ctx.scenario_logs_dir
        ctx.scenario_logs_dir.mkdir(parents=True, exist_ok=True)

        cucu_debug_log_path = ctx.scenario_logs_dir / "cucu.debug.console.log"
        ctx.scenario_debug_log_file = open(
            cucu_debug_log_path, "w", encoding=sys.stdout.encoding
        )
        ctx.scenario_debug_log_tee = TeeStream(ctx.scenario_debug_log_file)

        # redirect stdout, stderr and setup a logger at debug level to fill
        # the scenario cucu.debug.log file which makes it possible to have
        # debug logging for every single scenario run without polluting the
        # console logs at runtime.
        sys.stdout.set_other_stream(ctx.scenario_debug_log_tee)
        sys.stderr.set_other_stream(ctx.scenario_debug_log_tee)
        logger.init_debug_logger(ctx.scenario_debug_log_tee)

        # capture browser logs using TeeStream since each call clears the log
        ctx.browser_log_file = open(
            ctx.scenario_logs_dir / "browser_console.log.txt",
            "w",
            encoding="utf-8",
        )
        ctx.browser_log_tee = TeeStream(ctx.browser_log_file)

    # run before all scenario hooks
    for hook in CONFIG["__CUCU_BEFORE_SCENARIO_HOOKS"]:
        try:
            hook(ctx)
            logger.debug(f"HOOK {hook.__name__}: passed ✅")
        except Exception as e:
            error_message = (
                f"HOOK-ERROR in {hook.__name__}: {e.__class__.__name__}: {e}\n"
            )
            error_message += traceback.format_exc()
            logger.error(error_message)
            ctx.scenario.mark_skipped()
            # Set 'hook_failed' status to 'True' so that the test gets marked
            # as 'errored', even though no steps ran
            ctx.scenario.hook_failed = True


def run_after_scenario_hook(ctx, scenario, hook):
    try:
        hook(ctx)
        logger.debug(f"HOOK {hook.__name__}: passed ✅")
    except Exception as e:
        # For any after scenario hooks,'hook_failed' status will be 'False'
        # but will attach the error message to scenario.
        error_message = (
            f"HOOK-ERROR in {hook.__name__}: {e.__class__.__name__}: {e}\n"
        )
        error_message += traceback.format_exc()
        logger.error(error_message)


def after_scenario(ctx, scenario):
    for timer_name in ctx.step_timers:
        logger.warning(f'timer "{timer_name}" was never stopped/recorded')

    browser_info = {"has_browser": False}

    if len(ctx.browsers) != 0:
        try:
            tab_info = ctx.browser.get_tab_info()
            all_tabs = ctx.browser.get_all_tabs_info()
            browser_info = {
                "has_browser": True,
                "current_tab_index": tab_info["index"],
                "all_tabs": all_tabs,
                "browser_type": ctx.browser.driver.name,
                "session_id": ctx.browser.get_session_id(),
            }
        except Exception as e:
            logger.error(f"Error getting browser info: {e}")
    scenario.browser_info = browser_info

    run_after_scenario_hook(ctx, scenario, download_mht_data)

    # run after all scenario hooks in 'lifo' order.
    for hook in CONFIG["__CUCU_AFTER_SCENARIO_HOOKS"][::-1]:
        run_after_scenario_hook(ctx, scenario, hook)

    # run after this scenario hooks in 'lifo' order.
    for hook in CONFIG["__CUCU_AFTER_THIS_SCENARIO_HOOKS"][::-1]:
        run_after_scenario_hook(ctx, scenario, hook)

    CONFIG["__CUCU_AFTER_THIS_SCENARIO_HOOKS"] = []

    if CONFIG.true("CUCU_KEEP_BROWSER_ALIVE"):
        logger.debug("keeping browser alive between sessions")
    elif len(ctx.browsers) != 0:
        logger.debug("quitting browser between sessions")
        run_after_scenario_hook(ctx, scenario, cleanup_browsers)

    cucu_config_path = ctx.scenario_logs_dir / "cucu.config.yaml.txt"
    with open(cucu_config_path, "w") as config_file:
        config_file.write(CONFIG.to_yaml_without_secrets())

    scenario.cucu_config_json = yaml.safe_load(
        CONFIG.to_yaml_without_secrets()
    )

    scenario.end_at = get_iso_timestamp_with_ms()


def download_mht_data(ctx):
    if not ctx.browsers:
        logger.debug("No browsers - skipping MHT webpage snapshot")
    elif config.CONFIG["CUCU_BROWSER"].lower() != "chrome":
        logger.debug("Browser not Chrome - skipping MHT webpage snapshot")
    else:
        for index, browser in enumerate(ctx.browsers):
            mht_filename = (
                f"browser{index if len(ctx.browsers) > 1 else ''}_snapshot.mht"
            )
            mht_pathname = CONFIG["SCENARIO_LOGS_DIR"] / mht_filename
            logger.debug(f"Saving MHT webpage snapshot: {mht_filename}")
            browser.download_mht(mht_pathname)


def cleanup_browsers(ctx):
    # close the browser unless someone has set the keep browser alive
    # environment variable which allows tests to reuse the same browser
    # session

    for browser in ctx.browsers:
        browser.quit()

    ctx.browser_log_file.close()

    ctx.browsers = []


def before_step(ctx, step):
    step.start_at = get_iso_timestamp_with_ms()

    sys.stdout.captured()
    sys.stderr.captured()

    # Reset the debug log buffer for this step
    if hasattr(ctx, "scenario_debug_log_tee"):
        ctx.scenario_debug_log_tee.clear()

    ctx.current_step = step
    step.is_substep = getattr(step, "is_substep", False)
    step.has_substeps = getattr(step, "has_substeps", False)
    ctx.section_level = None
    step.seq = ctx.step_index + 1
    step.parent_seq = (
        ctx.section_step_stack[-1].seq if ctx.section_step_stack else 0
    )

    CONFIG["__STEP_SCREENSHOT_COUNT"] = 0

    # run before all step hooks
    for hook in CONFIG["__CUCU_BEFORE_STEP_HOOKS"]:
        hook(ctx)


def after_step(ctx, step):
    step.stdout = sys.stdout.captured()
    step.stderr = sys.stderr.captured()

    # Capture debug output from the TeeStream for this step
    if hasattr(ctx, "scenario_debug_log_tee"):
        step.debug_output = ctx.scenario_debug_log_tee.getvalue().splitlines()
    else:
        step.debug_output = []

    step.end_at = get_iso_timestamp_with_ms()

    # calculate duration from ISO timestamps
    ctx.scenario.previous_step_duration = (
        parse_iso_timestamp(step.end_at) - parse_iso_timestamp(step.start_at)
    ).total_seconds()

    # when set this means we're running in parallel mode using --workers and
    # we want to see progress reported using simply dots
    if CONFIG["__CUCU_PARENT_STDOUT"]:
        CONFIG["__CUCU_PARENT_STDOUT"].write(".")
        CONFIG["__CUCU_PARENT_STDOUT"].flush()

    # we only take screenshots of steps where there's a browser currently open
    # and this step has no substeps as in the reporting the substeps that
    # may actually do something on the browser take their own screenshots
    if ctx.browser is not None and ctx.current_step.has_substeps is False:
        take_screenshot(ctx, step.name, label=f"After {step.name}")

        tab_info = ctx.browser.get_tab_info()
        total_tabs = tab_info["tab_count"]
        current_tab = tab_info["index"] + 1
        title = tab_info["title"]
        url = tab_info["url"]
        log_message = (
            f"\ntab({current_tab} of {total_tabs}): {title}\nurl: {url}\n"
        )
        logger.debug(log_message)

        # Add tab info to step.stdout so it shows up in the HTML report
        step.stdout.extend(
            [f"tab({current_tab} of {total_tabs}): {title}", f"url: {url}"]
        )

    # if the step has substeps from using `run_steps` then we already moved
    # the step index in the run_steps method and shouldn't do it here
    if not step.has_substeps:
        ctx.step_index += 1
        CONFIG["__STEP_SCREENSHOT_COUNT"] = 0

    if CONFIG.bool("CUCU_DEBUG_ON_FAILURE") and step.status == "failed":
        ctx._runner.stop_capture()
        import pdb

        pdb.post_mortem(step.exc_traceback)

    CONFIG["__CUCU_BEFORE_THIS_SCENARIO_HOOKS"] = []

    # run after all step hooks
    for hook in CONFIG["__CUCU_AFTER_STEP_HOOKS"]:
        hook(ctx)

    # Capture browser logs and info for this step
    step.browser_logs = []
    browser_info = {"has_browser": False}
    if ctx.browser:
        for log in ctx.browser.get_log():
            log_entry = json.dumps(log)
            step.browser_logs.append(log)
            ctx.browser_log_tee.write(f"{log_entry}\n")

        tab_info = ctx.browser.get_tab_info()

        browser_info = {
            "tab_count": tab_info["tab_count"],
            "tab_number": tab_info["index"] + 1,
            "tab_title": tab_info["title"],
            "tab_url": tab_info["url"],
            "browser_type": ctx.browser.driver.name,
        }

    step.browser_info = browser_info
