import os
import sys
import time
import base64
import mimetypes
import logging
import argparse
import yaml
from datetime import datetime
from playwright.sync_api import sync_playwright

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))


def _find_test_suite():
    from_env = os.environ.get('OTEST_DIR')
    if from_env and os.path.isdir(from_env):
        return os.path.abspath(from_env)
    current = SCRIPT_DIR
    while current != os.path.dirname(current):
        candidate = os.path.join(current, 'Components', 'me', 'hannesnortje', 'TestSuite', '2.0.0')
        if os.path.isdir(candidate):
            return candidate
        current = os.path.dirname(current)
    raise RuntimeError("Cannot find TestSuite/2.0.0. Set OTEST_DIR or run via otest.")


sys.path.insert(0, _find_test_suite())
from utils.playwright_setup import create_playwright_context

with open(os.path.join(SCRIPT_DIR, 'otest.yaml')) as f:
    config = yaml.safe_load(f)

TARGET_URL = config['urls']['target']
DROP_ZONE_XPATH = config['xpaths']['target_drop_zone']
IMAGE_FIXTURE = config['fixtures']['image']
VCF_FIXTURE = config['fixtures']['vcf']
WAIT_TIME = config['settings']['wait_time']

SCREENSHOT_DIR = os.path.join(SCRIPT_DIR, 'latest')


def take_screenshot(page, name):
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    filename = os.path.join(SCREENSHOT_DIR, f"{name}_{timestamp}.png")
    page.screenshot(path=filename)
    logging.info(f"Screenshot saved: {filename}")


def local_file_drop(target_page, target_xpath, local_path, mime_type=None):
    """Drop a real local file onto an element via synthetic HTML5 drag-drop.

    Reads file bytes in Python, base64-encodes them, reconstructs a File inside
    the browser context, and dispatches dragenter/dragover/drop with a
    DataTransfer carrying that File. Required because Playwright cannot use
    the OS-level file-from-filesystem drag.
    """
    if not os.path.isfile(local_path):
        raise FileNotFoundError(f"Fixture not found: {local_path}")

    if mime_type is None:
        mime_type, _ = mimetypes.guess_type(local_path)
        if mime_type is None:
            mime_type = 'application/octet-stream'

    with open(local_path, 'rb') as f:
        data_b64 = base64.b64encode(f.read()).decode('ascii')
    file_name = os.path.basename(local_path)
    logging.info(f"Dropping {file_name} ({mime_type}, {len(data_b64)*3//4} bytes) onto {target_xpath}")

    target = target_page.locator(f"xpath={target_xpath}")
    target.wait_for(state="visible", timeout=10000)

    target.evaluate(
        """
        (el, args) => {
            const {b64, name, type} = args;
            const bin = atob(b64);
            const bytes = new Uint8Array(bin.length);
            for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
            const file = new File([bytes], name, {type});

            const dt = new DataTransfer();
            dt.items.add(file);

            const dragEnter = new DragEvent('dragenter', {bubbles:true, cancelable:true, dataTransfer:dt});
            el.dispatchEvent(dragEnter);
            const dragOver = new DragEvent('dragover', {bubbles:true, cancelable:true, dataTransfer:dt});
            dragOver.preventDefault = function() {};
            el.dispatchEvent(dragOver);
            const dropEv = new DragEvent('drop', {bubbles:true, cancelable:true, dataTransfer:dt});
            el.dispatchEvent(dropEv);
        }
        """,
        {'b64': data_b64, 'name': file_name, 'type': mime_type},
    )


def parse_vcf_fields(path):
    """Return a dict with FN, N, TEL, EMAIL fields if present in the vcf."""
    fields = {}
    with open(path, 'r', encoding='utf-8', errors='ignore') as f:
        for raw in f:
            line = raw.strip()
            for key in ('FN', 'N', 'TEL', 'EMAIL'):
                upper = line.upper()
                if upper.startswith(key + ':') or upper.startswith(key + ';'):
                    value = line.split(':', 1)[1] if ':' in line else ''
                    fields.setdefault(key, []).append(value.strip())
    return fields


def assert_image_rendered(page, container_xpath=None):
    """Find a visible, decoded <img> whose src is a blob: URL (the dropped file
    surfaced through URL.createObjectURL). Reject framework placeholders
    (PluginContainer 150x150, Web4Image's Circle.gif init default) and any non-blob
    src — the test verifies that the dropped file *content* actually rendered.
    Pick the largest blob: img by naturalWidth so we don't match a UI icon.
    """
    scope_xpath = container_xpath or "//body"
    info = page.evaluate(
        """xpath => {
            const ctx = document.evaluate(xpath, document, null,
                XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
            if (!ctx) return {error: 'no container'};
            const imgs = Array.from(ctx.querySelectorAll('img'));
            const candidates = imgs
                .filter(i => i.offsetWidth > 0 && i.naturalWidth > 0)
                .filter(i => (i.src || '').startsWith('blob:'))
                .map(i => ({src: (i.src || '').slice(-80), w: i.naturalWidth, h: i.naturalHeight, area: i.naturalWidth * i.naturalHeight}));
            candidates.sort((a, b) => b.area - a.area);
            return {total: imgs.length, candidates};
        }""",
        scope_xpath,
    )
    if info.get('error'):
        raise AssertionError(f"Container not found at {scope_xpath}")
    if not info['candidates']:
        raise AssertionError(
            f"No blob:-backed rendered <img> found (total imgs in container: {info['total']}). "
            "Likely the drop fell back to the Web4Image Circle.gif placeholder — the "
            "createFromMimeType(File|DataTransfer) branch in Web4Image.class.js may be missing."
        )
    best = info['candidates'][0]
    logging.info(f"SUCCESS: Image rendered ({best['w']}x{best['h']}, src=…{best['src']}).")
    return best


def assert_vcf_rendered(page, container_xpath, vcf_fields):
    text = page.locator(f"xpath={container_xpath or '//body'}").inner_text()

    fn_values = vcf_fields.get('FN', [])
    if fn_values and not any(v in text for v in fn_values if v):
        raise AssertionError(f"FN value(s) {fn_values!r} not in rendered DOM.")

    # PhoneNumber widget reformats the raw vcf string (e.g. "+49 8142 2917724"
    # becomes "+(49) 814-22917724"), so compare digits only.
    def digits(s):
        return ''.join(ch for ch in (s or '') if ch.isdigit())

    tel_values = [v for v in vcf_fields.get('TEL', []) if v]
    if tel_values:
        text_digits = digits(text)
        missing = [v for v in tel_values if digits(v) and digits(v) not in text_digits]
        if missing:
            raise AssertionError(
                f"Missing TEL values in rendered DOM (digit-normalized): {missing!r}. "
                f"Expected all {len(tel_values)} TEL value(s) from the vcf to appear "
                f"(parity with WODA Details-panel rendering)."
            )
        logging.info(f"SUCCESS: All {len(tel_values)} TEL value(s) rendered in DOM.")

    email_values = [v for v in vcf_fields.get('EMAIL', []) if v]
    if email_values:
        missing_email = [v for v in email_values if v not in text]
        if missing_email:
            raise AssertionError(
                f"Missing EMAIL values in rendered DOM: {missing_email!r}. "
                f"Expected all {len(email_values)} EMAIL value(s) from the vcf to appear "
                f"(emails should render as addresses, not JavaScriptObject[uuid])."
            )
        logging.info(f"SUCCESS: All {len(email_values)} EMAIL value(s) rendered in DOM.")

    if not tel_values and not email_values and not fn_values:
        raise AssertionError("vcf has no FN/TEL/EMAIL fields to assert against")


def perform_test(headless=False, browser_type='chromium'):
    browser = None
    playwright = None
    try:
        playwright = sync_playwright().start()
        browser, context = create_playwright_context(playwright, "TARGET", (0, 0), headless, browser_type=browser_type)
        page = context.new_page()
        logging.info(f"Loading target URL: {TARGET_URL}")
        page.goto(TARGET_URL)
        time.sleep(WAIT_TIME)
        take_screenshot(page, "1_initial")

        # --- Sub-step A: picture ---
        logging.info("\n" + "=" * 80 + "\nStep A: drop picture onto ONCE\n" + "=" * 80)
        image_path = os.path.join(SCRIPT_DIR, IMAGE_FIXTURE)
        local_file_drop(page, DROP_ZONE_XPATH, image_path)
        time.sleep(3)
        take_screenshot(page, "2_after_image_drop")
        assert_image_rendered(page, container_xpath=None)  # whole-page scan

        # Fresh page for the vcf sub-step
        logging.info("Reloading ONCE for clean vcf drop...")
        page.goto(TARGET_URL)
        time.sleep(WAIT_TIME)
        take_screenshot(page, "3_reloaded_for_vcf")

        # --- Sub-step B: vcf ---
        logging.info("\n" + "=" * 80 + "\nStep B: drop vcf onto ONCE\n" + "=" * 80)
        vcf_path = os.path.join(SCRIPT_DIR, VCF_FIXTURE)
        vcf_fields = parse_vcf_fields(vcf_path)
        logging.info(f"VCF fields parsed: {vcf_fields}")
        local_file_drop(page, DROP_ZONE_XPATH, vcf_path)
        time.sleep(3)
        take_screenshot(page, "4_after_vcf_drop")
        assert_vcf_rendered(page, container_xpath=None, vcf_fields=vcf_fields)

        logging.info("\n" + "=" * 80)
        logging.info("TEST COMPLETED SUCCESSFULLY!")
        logging.info("=" * 80)

    except Exception as e:
        logging.error(f"Test failed: {e}")
        raise
    finally:
        if browser:
            browser.close()
        if playwright:
            playwright.stop()


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Run ONCE file-drop tests (picture + vcf)')
    parser.add_argument('--headless', action='store_true', help='Run in headless mode')
    parser.add_argument('--browser', choices=['chromium', 'webkit'], default='chromium')
    args, _extra = parser.parse_known_args()

    suffix = '_ipad' if args.browser == 'webkit' else ''
    SCREENSHOT_DIR = os.path.join(SCRIPT_DIR, f'latest{suffix}')
    os.makedirs(SCREENSHOT_DIR, exist_ok=True)

    logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
    perform_test(args.headless, args.browser)
