"""The tests for the hassio component."""

from http import HTTPStatus

from aiohttp import StreamReader
from aiohttp.test_utils import TestClient
import pytest

from tests.common import MockUser
from tests.test_util.aiohttp import AiohttpClientMocker


@pytest.fixture
def hassio_user_client(
    hassio_client: TestClient, hass_admin_user: MockUser
) -> TestClient:
    """Return a Hass.io HTTP client tied to a non-admin user."""
    hass_admin_user.groups = []
    return hassio_client


@pytest.mark.parametrize(
    "path",
    [
        "addons/bl_b392/logo",
        "addons/bl_b392/icon",
    ],
)
async def test_forward_request_onboarded_user_get(
    hassio_user_client: TestClient, aioclient_mock: AiohttpClientMocker, path: str
) -> None:
    """Test fetching normal path."""
    aioclient_mock.get(f"http://127.0.0.1/{path}", text="response")

    resp = await hassio_user_client.get(f"/api/hassio/{path}")

    # Check we got right response
    assert resp.status == HTTPStatus.OK
    body = await resp.text()
    assert body == "response"

    # Check we forwarded command
    assert len(aioclient_mock.mock_calls) == 1
    # We only expect a single header.
    assert aioclient_mock.mock_calls[0][3] == {"X-Hass-Source": "core.http"}


@pytest.mark.parametrize("method", ["POST", "PUT", "DELETE"])
async def test_forward_request_onboarded_user_unallowed_methods(
    hassio_user_client: TestClient, aioclient_mock: AiohttpClientMocker, method: str
) -> None:
    """Test fetching normal path."""
    resp = await hassio_user_client.request(method, "/api/hassio/addons/bl_b392/icon")

    # Check we got right response
    assert resp.status == HTTPStatus.METHOD_NOT_ALLOWED

    # Check we did not forward command
    assert len(aioclient_mock.mock_calls) == 0


@pytest.mark.parametrize(
    ("bad_path", "expected_status"),
    [
        # Caught by bullshit filter
        ("addons/bl_b392/%252E./icon", HTTPStatus.BAD_REQUEST),
        # Unauthenticated path
        ("supervisor/info", HTTPStatus.UNAUTHORIZED),
        ("supervisor/logs", HTTPStatus.UNAUTHORIZED),
        ("supervisor/logs/follow", HTTPStatus.UNAUTHORIZED),
        ("addons/bl_b392/logs", HTTPStatus.UNAUTHORIZED),
        ("addons/bl_b392/logs/follow", HTTPStatus.UNAUTHORIZED),
    ],
)
async def test_forward_request_onboarded_user_unallowed_paths(
    hassio_user_client: TestClient,
    aioclient_mock: AiohttpClientMocker,
    bad_path: str,
    expected_status: int,
) -> None:
    """Test fetching normal path."""
    resp = await hassio_user_client.get(f"/api/hassio/{bad_path}")

    # Check we got right response
    assert resp.status == expected_status
    # Check we didn't forward command
    assert len(aioclient_mock.mock_calls) == 0


@pytest.mark.parametrize(
    "path",
    [
        "addons/bl_b392/logo",
        "addons/bl_b392/icon",
    ],
)
async def test_forward_request_onboarded_noauth_get(
    hassio_noauth_client: TestClient, aioclient_mock: AiohttpClientMocker, path: str
) -> None:
    """Test fetching normal path."""
    aioclient_mock.get(f"http://127.0.0.1/{path}", text="response")

    resp = await hassio_noauth_client.get(f"/api/hassio/{path}")

    # Check we got right response
    assert resp.status == HTTPStatus.OK
    body = await resp.text()
    assert body == "response"

    # Check we forwarded command
    assert len(aioclient_mock.mock_calls) == 1
    # We only expect a single header.
    assert aioclient_mock.mock_calls[0][3] == {"X-Hass-Source": "core.http"}


@pytest.mark.parametrize("method", ["POST", "PUT", "DELETE"])
async def test_forward_request_onboarded_noauth_unallowed_methods(
    hassio_noauth_client: TestClient, aioclient_mock: AiohttpClientMocker, method: str
) -> None:
    """Test fetching normal path."""
    resp = await hassio_noauth_client.request(method, "/api/hassio/addons/bl_b392/icon")

    # Check we got right response
    assert resp.status == HTTPStatus.METHOD_NOT_ALLOWED

    # Check we did not forward command
    assert len(aioclient_mock.mock_calls) == 0


@pytest.mark.parametrize(
    ("bad_path", "expected_status"),
    [
        # Caught by bullshit filter
        ("addons/bl_b392/%252E./icon", HTTPStatus.BAD_REQUEST),
        # Unauthenticated path
        ("supervisor/info", HTTPStatus.UNAUTHORIZED),
        ("supervisor/logs", HTTPStatus.UNAUTHORIZED),
        ("supervisor/logs/follow", HTTPStatus.UNAUTHORIZED),
        ("addons/bl_b392/logs", HTTPStatus.UNAUTHORIZED),
        ("addons/bl_b392/logs/follow", HTTPStatus.UNAUTHORIZED),
    ],
)
async def test_forward_request_onboarded_noauth_unallowed_paths(
    hassio_noauth_client: TestClient,
    aioclient_mock: AiohttpClientMocker,
    bad_path: str,
    expected_status: int,
) -> None:
    """Test fetching normal path."""
    resp = await hassio_noauth_client.get(f"/api/hassio/{bad_path}")

    # Check we got right response
    assert resp.status == expected_status
    # Check we didn't forward command
    assert len(aioclient_mock.mock_calls) == 0


BACKUP_NON_ADMIN_REJECTED_PARAMS = [
    ("GET", "backups/1234abcd/info"),
    ("GET", "backups/1234abcd/download"),
    ("POST", "backups/1234abcd/restore/full"),
    ("POST", "backups/1234abcd/restore/partial"),
    ("POST", "backups/new/upload"),
]


@pytest.mark.parametrize(("method", "path"), BACKUP_NON_ADMIN_REJECTED_PARAMS)
async def test_forward_request_backup_unauthenticated_rejected(
    hassio_noauth_client: TestClient,
    aioclient_mock: AiohttpClientMocker,
    method: str,
    path: str,
) -> None:
    """Test backup endpoints reject unauthenticated requests."""
    resp = await hassio_noauth_client.request(method, f"/api/hassio/{path}")

    assert resp.status == HTTPStatus.UNAUTHORIZED
    assert len(aioclient_mock.mock_calls) == 0


@pytest.mark.parametrize(("method", "path"), BACKUP_NON_ADMIN_REJECTED_PARAMS)
async def test_forward_request_backup_non_admin_rejected(
    hassio_user_client: TestClient,
    aioclient_mock: AiohttpClientMocker,
    method: str,
    path: str,
) -> None:
    """Test backup endpoints reject authenticated non-admin requests."""
    resp = await hassio_user_client.request(method, f"/api/hassio/{path}")

    assert resp.status == HTTPStatus.UNAUTHORIZED
    assert len(aioclient_mock.mock_calls) == 0


@pytest.mark.parametrize(
    ("path", "authenticated"),
    [
        ("addons/bl_b392/logo", False),
        ("addons/bl_b392/icon", False),
        ("backups/1234abcd/info", True),
        ("supervisor/logs", True),
        ("supervisor/logs/follow", True),
        ("addons/bl_b392/logs", True),
        ("addons/bl_b392/logs/follow", True),
        ("addons/bl_b392/changelog", True),
        ("addons/bl_b392/documentation", True),
    ],
)
async def test_forward_request_admin_get(
    hassio_client: TestClient,
    aioclient_mock: AiohttpClientMocker,
    path: str,
    authenticated: bool,
) -> None:
    """Test fetching normal path."""
    aioclient_mock.get(f"http://127.0.0.1/{path}", text="response")

    resp = await hassio_client.get(f"/api/hassio/{path}")

    # Check we got right response
    assert resp.status == HTTPStatus.OK
    body = await resp.text()
    assert body == "response"

    # Check we forwarded command
    assert len(aioclient_mock.mock_calls) == 1
    expected_headers = {
        "X-Hass-Source": "core.http",
    }
    if authenticated:
        expected_headers["Authorization"] = "Bearer 123456"

    assert aioclient_mock.mock_calls[0][3] == expected_headers


@pytest.mark.parametrize(
    "path",
    [
        "backups/new/upload",
        "backups/1234abcd/restore/full",
        "backups/1234abcd/restore/partial",
    ],
)
async def test_forward_request_admin_post(
    hassio_client: TestClient,
    aioclient_mock: AiohttpClientMocker,
    path: str,
) -> None:
    """Test fetching normal path."""
    aioclient_mock.get(f"http://127.0.0.1/{path}", text="response")

    resp = await hassio_client.get(f"/api/hassio/{path}")

    # Check we got right response
    assert resp.status == HTTPStatus.OK
    body = await resp.text()
    assert body == "response"

    # Check we forwarded command
    assert len(aioclient_mock.mock_calls) == 1
    # We only expect a single header.
    assert aioclient_mock.mock_calls[0][3] == {
        "X-Hass-Source": "core.http",
        "Authorization": "Bearer 123456",
    }


@pytest.mark.parametrize("method", ["POST", "PUT", "DELETE"])
async def test_forward_request_admin_unallowed_methods(
    hassio_client: TestClient, aioclient_mock: AiohttpClientMocker, method: str
) -> None:
    """Test fetching normal path."""
    resp = await hassio_client.request(method, "/api/hassio/addons/bl_b392/icon")

    # Check we got right response
    assert resp.status == HTTPStatus.METHOD_NOT_ALLOWED

    # Check we did not forward command
    assert len(aioclient_mock.mock_calls) == 0


@pytest.mark.parametrize(
    ("bad_path", "expected_status"),
    [
        # Caught by bullshit filter
        ("addons/bl_b392/%252E./icon", HTTPStatus.BAD_REQUEST),
        # Unauthenticated path
        ("supervisor/info", HTTPStatus.UNAUTHORIZED),
    ],
)
async def test_forward_request_admin_unallowed_paths(
    hassio_client: TestClient,
    aioclient_mock: AiohttpClientMocker,
    bad_path: str,
    expected_status: int,
) -> None:
    """Test fetching normal path."""
    resp = await hassio_client.get(f"/api/hassio/{bad_path}")

    # Check we got right response
    assert resp.status == expected_status
    # Check we didn't forward command
    assert len(aioclient_mock.mock_calls) == 0


async def test_bad_gateway_when_cannot_find_supervisor(
    hassio_client: TestClient, aioclient_mock: AiohttpClientMocker
) -> None:
    """Test we get a bad gateway error if we can't find supervisor."""
    aioclient_mock.get("http://127.0.0.1/addons/bl_b392/icon", exc=TimeoutError)

    resp = await hassio_client.get("/api/hassio/addons/bl_b392/icon")
    assert resp.status == HTTPStatus.BAD_GATEWAY


async def test_backup_upload_headers(
    hassio_client: TestClient,
    aioclient_mock: AiohttpClientMocker,
) -> None:
    """Test that we forward the full header for backup upload."""
    content_type = "multipart/form-data; boundary='--webkit'"
    aioclient_mock.post("http://127.0.0.1/backups/new/upload")

    resp = await hassio_client.post(
        "/api/hassio/backups/new/upload", headers={"Content-Type": content_type}
    )

    # Check we got right response
    assert resp.status == HTTPStatus.OK

    assert len(aioclient_mock.mock_calls) == 1

    req_headers = aioclient_mock.mock_calls[0][-1]
    assert req_headers["Content-Type"] == content_type


async def test_backup_download_headers(
    hassio_client: TestClient, aioclient_mock: AiohttpClientMocker
) -> None:
    """Test that we forward the full header for backup download."""
    content_disposition = "attachment; filename=test.tar"
    aioclient_mock.get(
        "http://127.0.0.1/backups/1234abcd/download",
        headers={
            "Content-Length": "50000000",
            "Content-Disposition": content_disposition,
        },
    )

    resp = await hassio_client.get("/api/hassio/backups/1234abcd/download")

    # Check we got right response
    assert resp.status == HTTPStatus.OK

    assert len(aioclient_mock.mock_calls) == 1

    assert resp.headers["Content-Disposition"] == content_disposition


async def test_stream(
    hassio_client: TestClient, aioclient_mock: AiohttpClientMocker
) -> None:
    """Verify that the request is a stream."""
    content_type = "multipart/form-data; boundary='--webkit'"
    aioclient_mock.post("http://127.0.0.1/backups/new/upload")
    resp = await hassio_client.post(
        "/api/hassio/backups/new/upload", headers={"Content-Type": content_type}
    )
    # Check we got right response
    assert resp.status == HTTPStatus.OK
    assert isinstance(aioclient_mock.mock_calls[-1][2], StreamReader)


async def test_simple_get_no_stream(
    hassio_client: TestClient, aioclient_mock: AiohttpClientMocker
) -> None:
    """Verify that a simple GET request is not a stream."""
    aioclient_mock.get("http://127.0.0.1/addons/bl_b392/icon")
    resp = await hassio_client.get("/api/hassio/addons/bl_b392/icon")
    assert resp.status == HTTPStatus.OK
    assert aioclient_mock.mock_calls[-1][2] is None


async def test_no_follow_logs_compress(
    hassio_client: TestClient, aioclient_mock: AiohttpClientMocker
) -> None:
    """Test that we do not compress follow logs."""
    aioclient_mock.get(
        "http://127.0.0.1/supervisor/logs/follow",
        headers={"Content-Type": "text/plain"},
    )
    aioclient_mock.get(
        "http://127.0.0.1/supervisor/logs",
        headers={"Content-Type": "text/plain"},
    )

    resp1 = await hassio_client.get("/api/hassio/supervisor/logs/follow")
    resp2 = await hassio_client.get("/api/hassio/supervisor/logs")

    # Check we got right response
    assert resp1.status == HTTPStatus.OK
    assert resp1.headers.get("Content-Encoding") is None

    assert resp2.status == HTTPStatus.OK
    assert resp2.headers.get("Content-Encoding") == "deflate"


async def test_no_event_stream_compress(
    hassio_client: TestClient, aioclient_mock: AiohttpClientMocker
) -> None:
    """Test that we do not compress SSE (Server-Sent Events) streams."""
    aioclient_mock.get(
        "http://127.0.0.1/backups/1234abcd/info",
        headers={"Content-Type": "text/event-stream"},
    )
    aioclient_mock.get(
        "http://127.0.0.1/addons/bl_b392/changelog",
        headers={"Content-Type": "application/json"},
    )

    resp1 = await hassio_client.get("/api/hassio/backups/1234abcd/info")
    resp2 = await hassio_client.get("/api/hassio/addons/bl_b392/changelog")

    # Check we got right response
    assert resp1.status == HTTPStatus.OK
    # SSE (text/event-stream) should not be compressed to allow streaming
    assert resp1.headers.get("Content-Encoding") is None

    assert resp2.status == HTTPStatus.OK
    # Regular JSON should be compressed
    assert resp2.headers.get("Content-Encoding") == "deflate"


async def test_forward_range_header_for_logs(
    hassio_client: TestClient, aioclient_mock: AiohttpClientMocker
) -> None:
    """Test that we forward the Range header for logs."""
    aioclient_mock.get("http://127.0.0.1/host/logs")
    aioclient_mock.get("http://127.0.0.1/host/logs/boots/-1")
    aioclient_mock.get("http://127.0.0.1/host/logs/boots/-2/follow?lines=100")
    aioclient_mock.get("http://127.0.0.1/addons/123abc_esphome/logs")
    aioclient_mock.get("http://127.0.0.1/addons/123abc_esphome/logs/follow")
    aioclient_mock.get("http://127.0.0.1/backups/1234abcd/download")

    test_range = ":-100:50"

    host_resp = await hassio_client.get(
        "/api/hassio/host/logs", headers={"Range": test_range}
    )
    host_resp2 = await hassio_client.get(
        "/api/hassio/host/logs/boots/-1", headers={"Range": test_range}
    )
    host_resp3 = await hassio_client.get(
        "/api/hassio/host/logs/boots/-2/follow?lines=100", headers={"Range": test_range}
    )
    addon_resp = await hassio_client.get(
        "/api/hassio/addons/123abc_esphome/logs", headers={"Range": test_range}
    )
    addon_resp2 = await hassio_client.get(
        "/api/hassio/addons/123abc_esphome/logs/follow", headers={"Range": test_range}
    )
    backup_resp = await hassio_client.get(
        "/api/hassio/backups/1234abcd/download", headers={"Range": test_range}
    )

    assert host_resp.status == HTTPStatus.OK
    assert host_resp2.status == HTTPStatus.OK
    assert host_resp3.status == HTTPStatus.OK
    assert addon_resp.status == HTTPStatus.OK
    assert addon_resp2.status == HTTPStatus.OK
    assert backup_resp.status == HTTPStatus.OK

    assert len(aioclient_mock.mock_calls) == 6

    assert aioclient_mock.mock_calls[0][-1].get("Range") == test_range
    assert aioclient_mock.mock_calls[1][-1].get("Range") == test_range
    assert aioclient_mock.mock_calls[2][-1].get("Range") == test_range
    assert aioclient_mock.mock_calls[3][-1].get("Range") == test_range
    assert aioclient_mock.mock_calls[4][-1].get("Range") == test_range
    assert aioclient_mock.mock_calls[5][-1].get("Range") is None
