import datetime
import socket
import threading
from typing import Optional
from wsgiref import simple_server

from pyramid.authentication import AuthTktCookieHelper
from pyramid.testing import DummyRequest

from testutils import load_gpx_asset
from fietsboek import models, util, actions
from fietsboek.models.track import Visibility, TrackType
from fietsboek.config import Config

import pytest


@pytest.fixture(scope="module")
def server_port():
    """Return a (likely) free port.

    Note that due to OS race conditions between picking the port and opening
    something on it, it might be taken again, but that is unlikely.
    """
    sock = socket.socket(socket.AF_INET)
    sock.bind(("", 0))
    port = sock.getsockname()[1]
    sock.close()
    return port


@pytest.fixture(scope="module", autouse=True)
def running_server(server_port, app):
    """Have the app running as an actual server."""
    server = simple_server.make_server("127.0.0.1", server_port, app)
    thread = threading.Thread(target=server.serve_forever)
    thread.daemon = True
    thread.start()
    yield
    server.shutdown()


@pytest.fixture
def browser_context_args(server_port) -> dict:
    return {"base_url": f"http://localhost:{server_port}"}


@pytest.fixture
def dbaccess(app):
    """Provide direct access to the database.

    This is needed for the selenium tests, because the normal "dbsession"
    fixture has a doomed transaction attached. This is nice for keeping the
    test database clean, but it does mean that the changes are not propagated
    through and the running WSGI app cannot read them.
    """
    session_factory = app.registry["dbsession_factory"]
    return session_factory()


class Helper:
    """Helper to insert objects for testing into the database and similar."""

    def __init__(self, dbaccess, app_settings, page, data_manager):
        self.dbaccess = dbaccess
        self.app_settings = app_settings
        self.page = page
        self.data_manager = data_manager
        self._johnny = None

    def john_doe(self) -> models.User:
        """Provides a test user (John Doe).

        This fixture either returns the existing John or creates a new one.
        """
        if self._johnny:
            return self._johnny

        with self.dbaccess:
            user = models.User(name="John Doe", email="john@doe.com", is_verified=True)
            user.set_password("password")
            self.dbaccess.add(user)
            self.dbaccess.commit()
            self.dbaccess.refresh(user, ["id", "email", "password", "session_secret"])
            self.dbaccess.expunge(user)
            self._johnny = user
            return user

    def login(self, user: Optional[models.User] = None):
        """Logs the given user in by setting the auth cookie."""
        if user is None:
            user = self.john_doe()
        config = Config.model_construct(session_key=self.app_settings["session_key"])
        secret = config.derive_secret("auth-cookie")
        helper = AuthTktCookieHelper(secret)
        headers = helper.remember(DummyRequest(), user.authenticated_user_id())
        for _, header_val in headers:
            cookie = header_val.split(";")[0]
            name, value = cookie.split("=", 1)
            self.page.context.add_cookies(
                [
                    {"name": name, "value": value, "domain": "localhost", "path": "/"},
                ]
            )

    def add_track(
        self, user: Optional[models.User] = None, track_name: str = "Teasi_1.gpx.gz"
    ) -> models.Track:
        """Add a track to the given user.

        If the user is None, John Doe is used.
        """
        if user is None:
            user = self.john_doe()
        with self.dbaccess:
            user = self.dbaccess.merge(user)
            track = actions.add_track(
                self.dbaccess,
                self.data_manager,
                owner=user,
                title="Another awesome track",
                visibility=Visibility.PRIVATE,
                description="Another description",
                track_type=TrackType.ORGANIC,
                date=datetime.datetime(2022, 12, 21, 17, 5, tzinfo=datetime.timezone.utc),
                tags=[],
                badges=[],
                tagged_people=[],
                transformers=[],
                gpx_data=load_gpx_asset(track_name),
            )
            self.dbaccess.commit()
            self.dbaccess.refresh(track, ["id", "link_secret"])
            self.dbaccess.expunge(track)
            return track


@pytest.fixture
def playwright_helper(dbaccess, app_settings, page, data_manager):
    return Helper(dbaccess, app_settings, page, data_manager)