import itertools
import os
import time

from datetime import datetime
from django.conf import settings
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from channels.testing import ChannelsLiveServerTestCase
from pathlib import Path
from selenium import webdriver
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.common.by import By

from .container_commands import create_session_on_server, reset_database
from .management.commands.create_session import create_pre_authenticated_session
from apps.applets.models import Applet



MAX_WAIT = 10
SCREEN_DUMP_LOCATION = Path(__file__).absolute().parent / "screendumps"


# Decorator fns
def wait(fn):
    def modified_fn(*args, **kwargs):
        start_time = time.time()
        while True:
            try:
                return fn(*args, **kwargs)
            except (AssertionError, WebDriverException) as e:
                if time.time() - start_time > MAX_WAIT:
                    raise e
                time.sleep(0.5)
    return modified_fn

# Functional Tests
class FunctionalTest(StaticLiveServerTestCase):
    # Helper methods
    def _make_browser(self, width=1366, height=900):
        """Create a Firefox instance sized to width×height."""
        options = webdriver.FirefoxOptions()
        if os.environ.get("HEADLESS"):
            options.add_argument("--headless")
        try:
            browser = webdriver.Firefox(options=options)
        except WebDriverException as e:
            # Geckodriver spawn race under --parallel: another worker may be
            # mid-spawn and the binary briefly reports "wrong permissions".
            # One sleep+retry absorbs the race; a genuine install fault will
            # fail both attempts. See pipeline #302/#303.
            if "geckodriver" not in str(e):
                raise
            time.sleep(1)
            browser = webdriver.Firefox(options=options)
        browser.set_window_size(width, height)
        return browser

    def setUp(self):
        self.browser = self._make_browser(1366, 900)
        self.test_server = os.environ.get("TEST_SERVER")
        if self.test_server:
            self.live_server_url = 'http://' + self.test_server
            reset_database(self.test_server)
        Applet.objects.get_or_create(slug="new-post", defaults={"name": "New Post", "context": "billboard"})

    def tearDown(self):
        if self._test_has_failed():
            if not SCREEN_DUMP_LOCATION.exists():
                SCREEN_DUMP_LOCATION.mkdir(parents=True)
            self.take_screenshot()
            self.dump_html()
        self.browser.quit()
        super().tearDown()

    def _test_has_failed(self):
        try:
            return any(
                failure[0] == self
                for failure in itertools.chain(
                    self._outcome.result.failures, self._outcome.result.errors
                )
            )
        except TypeError:
            return False
    
    def take_screenshot(self):
        path = SCREEN_DUMP_LOCATION / self._get_filename("png")
        print("screendumping to", path)
        self.browser.get_screenshot_as_file(str(path))

    def dump_html(self):
        path = SCREEN_DUMP_LOCATION / self._get_filename("html")
        print("dumping page html to", path)
        path.write_text(self.browser.page_source, encoding="utf-8")

    def _get_filename(self, extension):
        timestamp = datetime.now().isoformat().replace(":", ".")
        return (
            f"{self.__class__.__name__}.{self._testMethodName}-{timestamp}.{extension}"
        )


    @wait
    def wait_for(self, fn):
        return fn()

    def wait_for_slow(self, fn, timeout=30):
        start_time = time.time()
        while True:
            try:
                return fn()
            except (AssertionError, WebDriverException) as e:
                if time.time() - start_time > timeout:
                    raise e
                time.sleep(0.5)
        
    def create_pre_authenticated_session(self, email):
        if self.test_server:
            session_key = create_session_on_server(self.test_server, email)
        else:
            session_key = create_pre_authenticated_session(email)
        ## to set a cookie we need to first visit the domain
        ## 404 pages load the quickest!
        self.browser.get(self.live_server_url + "/404_no_such_url/")
        self.browser.add_cookie(
            dict(
                name=settings.SESSION_COOKIE_NAME,
                value=session_key,
                path="/",
            )
        )
        
    def confirm_guard(self, browser=None):
        b = browser or self.browser
        def _click():
            btn = b.find_element(By.CSS_SELECTOR, "#id_guard_portal.active .guard-yes")
            b.execute_script("arguments[0].click()", btn)
        self.wait_for(_click)

    def dismiss_brief_if_present(self, banner_selector=".note-banner", browser=None):
        """Remove any Brief banners matching `banner_selector` from the DOM so
        subsequent clicks aren't intercepted by the slide-down overlay. No-op
        if no matching banner exists. Default matches every Brief shape; pass
        a specific selector (e.g. '.my-sea-sign-gate-brief') to target one.

        DOM-removal rather than NVM-click — bypasses the dismiss_url POST
        flow that some Briefs (FREE/PAID DRAW) wire up; use this when the
        test cares about the page state AFTER a Brief, not the dismissal."""
        b = browser or self.browser
        b.execute_script(
            f"document.querySelectorAll({banner_selector!r}).forEach(el => el.remove());"
        )

    @wait
    def wait_to_be_logged_in(self, email):
        self.browser.find_element(By.CSS_SELECTOR, "#id_logout"),
        navbar = self.browser.find_element(By.CSS_SELECTOR, ".navbar")
        self.assertIn(email, navbar.text)

    @wait
    def wait_to_be_logged_out(self, email):
        lambda: self.browser.find_element(By.CSS_SELECTOR, "input[name=email]"),
        navbar = self.browser.find_element(By.CSS_SELECTOR, ".navbar")
        self.assertNotIn(email, navbar.text)


class ChannelsFunctionalTest(ChannelsLiveServerTestCase):
    """Like FunctionalTest but backed by daphne so WebSocket connections work."""
    serve_static = True

    def setUp(self):
        options = webdriver.FirefoxOptions()
        headless = os.environ.get("HEADLESS")
        if headless:
            options.add_argument("--headless")
        self.browser = webdriver.Firefox(options=options)
        self.browser.set_window_size(1366, 900)
        self.test_server = os.environ.get("TEST_SERVER")
        if self.test_server:
            self.live_server_url = 'http://' + self.test_server
            reset_database(self.test_server)
        Applet.objects.get_or_create(slug="new-post", defaults={"name": "New Post", "context": "billboard"})

    def tearDown(self):
        if self._test_has_failed():
            if not SCREEN_DUMP_LOCATION.exists():
                SCREEN_DUMP_LOCATION.mkdir(parents=True)
            self.take_screenshot()
            self.dump_html()
        self.browser.quit()
        super().tearDown()

    def _test_has_failed(self):
        try:
            return any(
                failure[0] == self
                for failure in itertools.chain(
                    self._outcome.result.failures, self._outcome.result.errors
                )
            )
        except TypeError:
            return False

    def take_screenshot(self):
        path = SCREEN_DUMP_LOCATION / self._get_filename("png")
        print("screendumping to", path)
        self.browser.get_screenshot_as_file(str(path))

    def dump_html(self):
        path = SCREEN_DUMP_LOCATION / self._get_filename("html")
        print("dumping page html to", path)
        path.write_text(self.browser.page_source, encoding="utf-8")

    def _get_filename(self, extension):
        timestamp = datetime.now().isoformat().replace(":", ".")
        return (
            f"{self.__class__.__name__}.{self._testMethodName}-{timestamp}.{extension}"
        )

    @wait
    def wait_for(self, fn, browser=None):
        return fn()

    def wait_for_slow(self, fn, timeout=30, browser=None):
        start_time = time.time()
        while True:
            try:
                return fn()
            except (AssertionError, WebDriverException) as e:
                if time.time() - start_time > timeout:
                    raise e
                time.sleep(0.5)

    def confirm_guard(self, browser=None):
        b = browser or self.browser
        def _click():
            btn = b.find_element(By.CSS_SELECTOR, "#id_guard_portal.active .guard-yes")
            b.execute_script("arguments[0].click()", btn)
        self.wait_for(_click)

    def dismiss_brief_if_present(self, banner_selector=".note-banner", browser=None):
        """Remove any Brief banners matching `banner_selector` from the DOM so
        subsequent clicks aren't intercepted by the slide-down overlay. No-op
        if no matching banner exists. See FunctionalTest.dismiss_brief_if_present
        for the full docstring — mirrored here for ChannelsLiveServerTestCase."""
        b = browser or self.browser
        b.execute_script(
            f"document.querySelectorAll({banner_selector!r}).forEach(el => el.remove());"
        )

    def create_pre_authenticated_session(self, email):
        if self.test_server:
            session_key = create_session_on_server(self.test_server, email)
        else:
            session_key = create_pre_authenticated_session(email)
        self.browser.get(self.live_server_url + "/404_no_such_url/")
        self.browser.add_cookie(
            dict(
                name=settings.SESSION_COOKIE_NAME,
                value=session_key,
                path="/",
            )
        )
