"""Proxy and Outpost e2e tests"""

from base64 import b64encode
from dataclasses import asdict
from json import dumps
from time import sleep

from jwt import decode
from selenium.webdriver.common.by import By

from authentik.blueprints.tests import apply_blueprint, reconcile_app
from authentik.core.models import Application
from authentik.flows.models import Flow
from authentik.lib.generators import generate_id
from authentik.outposts.models import Outpost, OutpostConfig, OutpostType
from authentik.providers.proxy.models import ProxyProvider
from tests.decorators import retry
from tests.live import ChannelsE2ETestCase
from tests.selenium import SeleniumTestCase


class TestProviderProxy(SeleniumTestCase):
    """Proxy and Outpost e2e tests"""

    def setUp(self):
        super().setUp()
        self.run_container(
            image="traefik/whoami:latest",
            ports={
                "80": "80",
            },
        )

    def start_proxy(self, outpost: Outpost):
        """Start proxy container based on outpost created"""
        self.run_container(
            image=self.get_container_image("ghcr.io/goauthentik/dev-proxy"),
            ports={
                "9000": "9000",
            },
            environment={
                "AUTHENTIK_TOKEN": outpost.token.key,
            },
        )

    @retry()
    @apply_blueprint(
        "default/flow-default-authentication-flow.yaml",
        "default/flow-default-invalidation-flow.yaml",
    )
    @apply_blueprint(
        "default/flow-default-provider-authorization-implicit-consent.yaml",
        "default/flow-default-provider-invalidation.yaml",
    )
    @apply_blueprint(
        "system/providers-oauth2.yaml",
        "system/providers-proxy.yaml",
    )
    @reconcile_app("authentik_crypto")
    def test_proxy_simple(self):
        """Test simple outpost setup with single provider"""
        # set additionalHeaders to test later
        self.user.attributes["additionalHeaders"] = {"X-Foo": "bar"}
        self.user.save()

        proxy: ProxyProvider = ProxyProvider.objects.create(
            name=generate_id(),
            authorization_flow=Flow.objects.get(
                slug="default-provider-authorization-implicit-consent"
            ),
            invalidation_flow=Flow.objects.get(slug="default-provider-invalidation-flow"),
            internal_host=f"http://{self.host}",
            external_host="http://localhost:9000",
        )
        # Ensure OAuth2 Params are set
        proxy.set_oauth_defaults()
        proxy.save()
        # we need to create an application to actually access the proxy
        Application.objects.create(name=generate_id(), slug=generate_id(), provider=proxy)
        outpost: Outpost = Outpost.objects.create(
            name=generate_id(),
            type=OutpostType.PROXY,
        )
        outpost.providers.add(proxy)
        outpost.build_user_permissions(outpost.user)

        self.start_proxy(outpost)

        sleep(5)

        self.driver.get("http://localhost:9000/api")
        self.login()
        sleep(1)

        body = self.parse_json_content()
        headers = body.get("headers", {})
        snippet = dumps(body, indent=2)[:500].replace("\n", " ")

        self.assertEqual(
            headers.get("X-Authentik-Username"),
            [self.user.username],
            f"X-Authentik-Username header mismatch at {self.driver.current_url}: {snippet}",
        )
        self.assertEqual(
            headers.get("X-Foo"),
            ["bar"],
            f"X-Foo header mismatch at {self.driver.current_url}: {snippet}",
        )
        raw_jwt: str = headers.get("X-Authentik-Jwt", [None])[0]
        jwt = decode(raw_jwt, options={"verify_signature": False})

        self.assertIsNotNone(jwt["sid"], "Missing 'sid' in JWT")
        self.assertIsNotNone(jwt["ak_proxy"], "Missing 'ak_proxy' in JWT")

        self.driver.get("http://localhost:9000/outpost.goauthentik.io/sign_out")
        sleep(2)

        flow_executor = self.get_shadow_root("ak-flow-executor")
        session_end_stage = self.get_shadow_root("ak-stage-session-end", flow_executor)
        flow_card = self.get_shadow_root("ak-flow-card", session_end_stage)
        title = flow_card.find_element(By.CSS_SELECTOR, ".pf-c-title.pf-m-3xl").text

        self.assertIn(
            "You've logged out of",
            title,
            f"Logout title mismatch at {self.driver.current_url}: {title}",
        )

    @retry()
    @apply_blueprint(
        "default/flow-default-authentication-flow.yaml",
        "default/flow-default-invalidation-flow.yaml",
    )
    @apply_blueprint(
        "default/flow-default-provider-authorization-implicit-consent.yaml",
        "default/flow-default-provider-invalidation.yaml",
    )
    @apply_blueprint(
        "system/providers-oauth2.yaml",
        "system/providers-proxy.yaml",
    )
    @reconcile_app("authentik_crypto")
    def test_proxy_basic_auth(self):
        """Test simple outpost setup with single provider"""
        cred = generate_id()
        attr = "basic-password"  # nosec
        self.user.attributes["basic-username"] = cred
        self.user.attributes[attr] = cred
        self.user.save()

        proxy: ProxyProvider = ProxyProvider.objects.create(
            name=generate_id(),
            authorization_flow=Flow.objects.get(
                slug="default-provider-authorization-implicit-consent"
            ),
            invalidation_flow=Flow.objects.get(slug="default-provider-invalidation-flow"),
            internal_host=f"http://{self.host}",
            external_host="http://localhost:9000",
            basic_auth_enabled=True,
            basic_auth_user_attribute="basic-username",
            basic_auth_password_attribute=attr,
        )
        # Ensure OAuth2 Params are set
        proxy.set_oauth_defaults()
        proxy.save()
        # we need to create an application to actually access the proxy
        Application.objects.create(name=generate_id(), slug=generate_id(), provider=proxy)
        outpost: Outpost = Outpost.objects.create(
            name=generate_id(),
            type=OutpostType.PROXY,
        )
        outpost.providers.add(proxy)
        outpost.build_user_permissions(outpost.user)

        self.start_proxy(outpost)

        sleep(5)

        self.driver.get("http://localhost:9000/api")
        self.login()
        sleep(1)

        body = self.parse_json_content()
        headers = body.get("headers", {})
        snippet = dumps(body, indent=2)[:500].replace("\n", " ")

        self.assertEqual(
            headers.get("X-Authentik-Username"),
            [self.user.username],
            f"X-Authentik-Username header mismatch at {self.driver.current_url}: {snippet}",
        )

        auth_header = b64encode(f"{cred}:{cred}".encode()).decode()

        self.assertEqual(
            headers.get("Authorization"),
            [f"Basic {auth_header}"],
            f"Authorization header mismatch at {self.driver.current_url}: {snippet}",
        )

        self.driver.get("http://localhost:9000/outpost.goauthentik.io/sign_out")
        sleep(2)

        flow_executor = self.get_shadow_root("ak-flow-executor")
        session_end_stage = self.get_shadow_root("ak-stage-session-end", flow_executor)
        flow_card = self.get_shadow_root("ak-flow-card", session_end_stage)
        title = flow_card.find_element(By.CSS_SELECTOR, ".pf-c-title.pf-m-3xl").text

        self.assertIn(
            "You've logged out of",
            title,
            f"Logout title mismatch at {self.driver.current_url}: {title}",
        )


class TestProviderProxyConnect(ChannelsE2ETestCase):
    """Test Proxy connectivity over websockets"""

    @retry(exceptions=[AssertionError])
    @apply_blueprint(
        "default/flow-default-authentication-flow.yaml",
        "default/flow-default-invalidation-flow.yaml",
    )
    @apply_blueprint(
        "default/flow-default-provider-authorization-implicit-consent.yaml",
    )
    @reconcile_app("authentik_crypto")
    def test_proxy_connectivity(self):
        """Test proxy connectivity over websocket"""
        proxy: ProxyProvider = ProxyProvider.objects.create(
            name=generate_id(),
            authorization_flow=Flow.objects.get(
                slug="default-provider-authorization-implicit-consent"
            ),
            internal_host="http://localhost",
            external_host="http://localhost:9000",
        )
        # Ensure OAuth2 Params are set
        proxy.set_oauth_defaults()
        proxy.save()
        # we need to create an application to actually access the proxy
        Application.objects.create(name=generate_id(), slug=generate_id(), provider=proxy)

        outpost: Outpost = Outpost.objects.create(
            name=generate_id(),
            type=OutpostType.PROXY,
            _config=asdict(OutpostConfig(authentik_host=self.live_server_url, log_level="debug")),
        )
        outpost.providers.add(proxy)
        outpost.build_user_permissions(outpost.user)

        self.run_container(
            image=self.get_container_image("ghcr.io/goauthentik/dev-proxy"),
            ports={
                "9000": "9000",
            },
            environment={
                "AUTHENTIK_TOKEN": outpost.token.key,
            },
        )

        # Wait until outpost healthcheck succeeds
        healthcheck_retries = 0
        while healthcheck_retries < 50:  # noqa: PLR2004
            if len(outpost.state) > 0:
                state = outpost.state[0]
                if state.last_seen and state.version:
                    break
            healthcheck_retries += 1
            sleep(0.5)

        state = outpost.state
        self.assertGreaterEqual(len(state), 1)

        # Make sure to delete the outpost to remove the container
        outpost.delete()
