"""Integration tests for the real Gencat + IsardVDI flows.

These tests are skipped by default.  To run them:

    uv run pytest tests/test_integration.py -v -s

They require network access and valid credentials (via .env or env vars).
"""

from __future__ import annotations

import os
import time

import pytest
from dotenv import load_dotenv

from isard import (
    IsardVdiClient,
    IsardVdiSession,
    add_ssh_key,
    get_bastion,
    get_desktops,
    login_to_gencat,
    set_bastion,
    star_desktop,
    start_desktop,
    stop_desktop,
)
from isard.errors import BookingRequiredError, HttpError
from isard.types import BastionTarget

# Load .env file if present (credentials for local development).
load_dotenv()

# Credentials can be overridden via environment variables for CI use.
USERNAME = os.environ.get("GENCAT_USERNAME", "")
PASSWORD = os.environ.get("GENCAT_PASSWORD", "")


_CREDS_AVAILABLE = bool(USERNAME and PASSWORD)
_SKIP_REASON = "Integration test: requires network + valid credentials (set GENCAT_USERNAME and GENCAT_PASSWORD)"


# ---------------------------------------------------------------------------
# Session-scoped fixture — log in once and share the session across tests
# ---------------------------------------------------------------------------


@pytest.fixture(scope="session")
def isardvdi_session() -> IsardVdiSession:
    """Return a single IsardVDI session, shared across all integration tests."""
    if not _CREDS_AVAILABLE:
        pytest.skip(_SKIP_REASON)
    client = IsardVdiClient.login(USERNAME, PASSWORD)
    return client.isardvdi_session


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


@pytest.mark.skipif(not _CREDS_AVAILABLE, reason=_SKIP_REASON)
def test_real_auth() -> None:
    """Full end-to-end SAML authentication against the live Gencat platform."""
    session = login_to_gencat(USERNAME, PASSWORD)

    assert session is not None
    assert session.is_valid(), "Session should be valid immediately after login"

    ui = session.user_info
    assert ui.username, "Username must be non-empty"
    assert "@" in ui.username, (
        f"Username should look like an email, got: {ui.username!r}"
    )
    assert ui.display_name, "Display name must be non-empty"
    assert ui.email, "Email must be non-empty"

    print()
    print(f"  username     : {ui.username}")
    print(f"  display_name : {ui.display_name}")
    print(f"  email        : {ui.email}")
    print(f"  given_name   : {ui.given_name}")
    print(f"  family_name  : {ui.family_name}")
    print(f"  organization : {ui.organization}")
    print(f"  role         : {ui.role}")
    print(f"  expires_at   : {session.expires_at}")
    print(f"  session_id   : {session.session_id}")


@pytest.mark.skipif(not _CREDS_AVAILABLE, reason=_SKIP_REASON)
def test_get_desktops(isardvdi_session: IsardVdiSession) -> None:
    """End-to-end test: log in to IsardVDI and retrieve the desktop list."""
    assert isardvdi_session.is_valid(), "IsardVDI session should be valid"

    desktops = get_desktops(isardvdi_session)

    # Basic structural checks — we can't assert specific names/states
    # since they depend on the live account.
    assert isinstance(desktops, list), "get_desktops() must return a list"
    for vm in desktops:
        assert vm.id, f"Desktop missing id: {vm!r}"
        assert vm.name, f"Desktop missing name: {vm!r}"
        assert vm.state, f"Desktop missing state: {vm!r}"

    print()
    print(f"  desktops found: {len(desktops)}")
    for vm in desktops:
        print(f"  - {vm.name:30s}  state={vm.state:15s}  ip={vm.ip or '-'}")


@pytest.mark.skipif(not _CREDS_AVAILABLE, reason=_SKIP_REASON)
def test_star_desktop(isardvdi_session: IsardVdiSession) -> None:
    """End-to-end test: star then unstar the first available desktop.

    This is fully reversible — the desktop's power state is not touched.
    """
    desktops = get_desktops(isardvdi_session)
    assert desktops, "Need at least one desktop to run this test"

    target = desktops[0]
    print()
    print(f"  target desktop: {target.name!r}  (id={target.id})")

    # Star it
    try:
        star_desktop(isardvdi_session, target.id, starred=True)
    except HttpError as exc:
        if exc.status_code == 428:
            pytest.skip(
                f"Desktop {target.name!r} requires a booking (HTTP 428) — skipping star test"
            )
        raise
    print("  starred OK")

    # Brief pause to let the server register the change
    time.sleep(1)

    # Unstar it (restore original state)
    star_desktop(isardvdi_session, target.id, starred=False)
    print("  unstarred OK")


@pytest.mark.skipif(not _CREDS_AVAILABLE, reason=_SKIP_REASON)
def test_stop_desktop(isardvdi_session: IsardVdiSession) -> None:
    """End-to-end test: stop a Started desktop, then restart it.

    Skipped at runtime if no desktop is currently in the Started state.
    The test restores the original state by restarting the desktop after stopping.
    """
    desktops = get_desktops(isardvdi_session)
    started = [d for d in desktops if d.state == "Started"]

    if not started:
        pytest.skip("No Started desktops available — skipping stop test")

    target = started[0]
    print()
    print(f"  target desktop: {target.name!r}  state={target.state}  id={target.id}")

    # Stop
    new_state = stop_desktop(isardvdi_session, target.id)
    print(f"  stop response state: {new_state!r}")
    assert new_state in ("Stopping", "Shutting-down", "Stopped", ""), (
        f"Unexpected stop response state: {new_state!r}"
    )

    # Restore: restart (best-effort — HTTP 428 means a booking is needed,
    # which is a server-side scheduling constraint, not a test failure)
    time.sleep(2)
    try:
        restart_state = start_desktop(isardvdi_session, target.id)
        print(f"  restart response state: {restart_state!r}")
    except (BookingRequiredError, HttpError) as exc:
        if isinstance(exc, BookingRequiredError) or (
            isinstance(exc, HttpError) and exc.status_code == 428
        ):
            print(f"  restart skipped: HTTP 428 (booking required)")
        else:
            raise


@pytest.mark.skipif(not _CREDS_AVAILABLE, reason=_SKIP_REASON)
def test_start_desktop(isardvdi_session: IsardVdiSession) -> None:
    """End-to-end test: start a Stopped desktop, then stop it again.

    Skipped at runtime if no desktop is currently in the Stopped state.
    The test restores the original state by stopping the desktop after starting.
    HTTP 428 during start is treated as a server-side booking constraint and
    does not fail the test.
    """
    desktops = get_desktops(isardvdi_session)
    stopped = [d for d in desktops if d.state == "Stopped"]

    if not stopped:
        pytest.skip("No Stopped desktops available — skipping start test")

    target = stopped[0]
    print()
    print(f"  target desktop: {target.name!r}  state={target.state}  id={target.id}")

    # Start (may return 428 if a booking is required)
    try:
        new_state = start_desktop(isardvdi_session, target.id)
        print(f"  start response state: {new_state!r}")
        assert new_state in ("Starting", "Started", ""), (
            f"Unexpected start response state: {new_state!r}"
        )
    except (BookingRequiredError, HttpError) as exc:
        if isinstance(exc, BookingRequiredError) or (
            isinstance(exc, HttpError) and exc.status_code == 428
        ):
            print(f"  start returned HTTP 428 (booking required) — test skipped")
            pytest.skip(
                "Desktop requires a booking before it can be started (HTTP 428)"
            )
        raise

    # Restore: stop again
    time.sleep(2)
    restore_state = stop_desktop(isardvdi_session, target.id)
    print(f"  restore stop state: {restore_state!r}")


# ---------------------------------------------------------------------------
# Bastion / SSH tests
# ---------------------------------------------------------------------------


@pytest.mark.skipif(not _CREDS_AVAILABLE, reason=_SKIP_REASON)
def test_bastion_allowed(isardvdi_session: IsardVdiSession) -> None:
    """Verify that the authenticated user is allowed to use the SSH bastion."""
    client = IsardVdiClient(isardvdi_session)
    allowed = client.is_bastion_allowed()
    print()
    print(f"  bastion allowed: {allowed}")
    assert isinstance(allowed, bool)


@pytest.mark.skipif(not _CREDS_AVAILABLE, reason=_SKIP_REASON)
def test_get_bastion(isardvdi_session: IsardVdiSession) -> None:
    """Fetch the bastion config for the first available desktop."""
    desktops = get_desktops(isardvdi_session)
    assert desktops, "Need at least one desktop"

    target_desktop = desktops[0]
    client = IsardVdiClient(isardvdi_session)
    target = client.get_bastion(target_desktop.id)

    print()
    print(f"  desktop:          {target_desktop.name!r}")
    print(f"  bastion id:       {target.id}")
    print(f"  ssh.enabled:      {target.ssh.enabled}")
    print(f"  ssh.port:         {target.ssh.port}")
    print(f"  authorized_keys:  {len(target.ssh.authorized_keys)} key(s)")

    assert isinstance(target, BastionTarget)
    assert target.id, "BastionTarget.id must be non-empty"
    assert target.desktop_id == target_desktop.id


@pytest.mark.skipif(not _CREDS_AVAILABLE, reason=_SKIP_REASON)
def test_set_bastion(isardvdi_session: IsardVdiSession) -> None:
    """Toggle SSH bastion on/off and verify the round-trip."""
    desktops = get_desktops(isardvdi_session)
    assert desktops, "Need at least one desktop"

    target_desktop = desktops[0]
    client = IsardVdiClient(isardvdi_session)
    original = client.get_bastion(target_desktop.id)

    print()
    print(f"  desktop:          {target_desktop.name!r}")
    print(f"  original enabled: {original.ssh.enabled}")

    # Toggle: enable if currently disabled, disable if enabled.
    new_enabled = not original.ssh.enabled
    updated = client.set_bastion(
        target_desktop.id,
        ssh_enabled=new_enabled,
        ssh_port=original.ssh.port,
        authorized_keys=original.ssh.authorized_keys,
    )
    print(f"  after toggle:     {updated.ssh.enabled}")
    assert updated.ssh.enabled == new_enabled

    # Restore original state.
    restored = client.set_bastion(
        target_desktop.id,
        ssh_enabled=original.ssh.enabled,
        ssh_port=original.ssh.port,
        authorized_keys=original.ssh.authorized_keys,
    )
    print(f"  after restore:    {restored.ssh.enabled}")
    assert restored.ssh.enabled == original.ssh.enabled


@pytest.mark.skipif(not _CREDS_AVAILABLE, reason=_SKIP_REASON)
def test_add_ssh_key(isardvdi_session: IsardVdiSession) -> None:
    """Add a test SSH key idempotently, verify presence, then remove it."""
    desktops = get_desktops(isardvdi_session)
    assert desktops, "Need at least one desktop"

    target_desktop = desktops[0]
    client = IsardVdiClient(isardvdi_session)
    original = client.get_bastion(target_desktop.id)

    test_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestKeyForIntegrationTestDoNotUse test@integration"

    print()
    print(f"  desktop:       {target_desktop.name!r}")
    print(f"  keys before:   {len(original.ssh.authorized_keys)}")

    # Add the test key.
    updated = client.add_ssh_key(target_desktop.id, test_key)
    print(f"  keys after:    {len(updated.ssh.authorized_keys)}")
    assert test_key in updated.ssh.authorized_keys, "Test key should be present"

    # Add again — must be idempotent (no duplicate).
    updated2 = client.add_ssh_key(target_desktop.id, test_key)
    count = updated2.ssh.authorized_keys.count(test_key)
    assert count == 1, f"Key must appear exactly once, got {count}"
    print(f"  idempotent:    OK (key appears {count} time)")

    # Restore original keys.
    restored = client.set_bastion(
        target_desktop.id,
        ssh_enabled=original.ssh.enabled,
        ssh_port=original.ssh.port,
        authorized_keys=original.ssh.authorized_keys,
    )
    assert test_key not in restored.ssh.authorized_keys, "Test key should be removed"
    print(f"  keys restored: {len(restored.ssh.authorized_keys)}")
