"""Test the Google Drive backup platform."""

from io import StringIO
import json
from typing import Any
from unittest.mock import AsyncMock, MagicMock, Mock, patch

from aiohttp import ClientResponse
from google_drive_api.exceptions import GoogleDriveApiError
import pytest
from syrupy.assertion import SnapshotAssertion

from homeassistant.components.backup import (
    DOMAIN as BACKUP_DOMAIN,
    AddonInfo,
    AgentBackup,
)
from homeassistant.components.google_drive import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component

from .conftest import CONFIG_ENTRY_TITLE, TEST_AGENT_ID

from tests.common import MockConfigEntry
from tests.test_util.aiohttp import mock_stream
from tests.typing import ClientSessionGenerator, WebSocketGenerator

FOLDER_ID = "google-folder-id"
TEST_AGENT_BACKUP = AgentBackup(
    addons=[AddonInfo(name="Test", slug="test", version="1.0.0")],
    backup_id="test-backup",
    database_included=True,
    date="2025-01-01T01:23:45.678Z",
    extra_metadata={
        "with_automatic_settings": False,
    },
    folders=[],
    homeassistant_included=True,
    homeassistant_version="2024.12.0",
    name="Test",
    protected=False,
    size=987,
)
TEST_AGENT_BACKUP_RESULT = {
    "addons": [{"name": "Test", "slug": "test", "version": "1.0.0"}],
    "agents": {TEST_AGENT_ID: {"protected": False, "size": 987}},
    "backup_id": "test-backup",
    "database_included": True,
    "date": "2025-01-01T01:23:45.678Z",
    "extra_metadata": {"with_automatic_settings": False},
    "failed_addons": [],
    "failed_agent_ids": [],
    "failed_folders": [],
    "folders": [],
    "homeassistant_included": True,
    "homeassistant_version": "2024.12.0",
    "name": "Test",
    "with_automatic_settings": None,
}


@pytest.fixture(autouse=True)
async def setup_integration(
    hass: HomeAssistant,
    config_entry: MockConfigEntry,
    mock_api: MagicMock,
) -> None:
    """Set up Google Drive integration."""
    config_entry.add_to_hass(hass)
    assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}})
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()
    mock_api.list_files = AsyncMock(
        return_value={"files": [{"id": "HA folder ID", "name": "HA folder name"}]}
    )


async def test_agents_info(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
) -> None:
    """Test backup agent info."""
    client = await hass_ws_client(hass)

    await client.send_json_auto_id({"type": "backup/agents/info"})
    response = await client.receive_json()

    assert response["success"]
    assert response["result"] == {
        "agents": [
            {"agent_id": "backup.local", "name": "local"},
            {"agent_id": TEST_AGENT_ID, "name": CONFIG_ENTRY_TITLE},
        ],
    }

    config_entry = hass.config_entries.async_entries(DOMAIN)[0]
    await hass.config_entries.async_unload(config_entry.entry_id)
    await hass.async_block_till_done()

    await client.send_json_auto_id({"type": "backup/agents/info"})
    response = await client.receive_json()

    assert response["success"]
    assert response["result"] == {
        "agents": [{"agent_id": "backup.local", "name": "local"}]
    }


async def test_agents_list_backups(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    mock_api: MagicMock,
    snapshot: SnapshotAssertion,
) -> None:
    """Test agent list backups."""
    mock_api.list_files = AsyncMock(
        return_value={
            "files": [{"description": json.dumps(TEST_AGENT_BACKUP.as_dict())}]
        }
    )

    client = await hass_ws_client(hass)
    await client.send_json_auto_id({"type": "backup/info"})
    response = await client.receive_json()

    assert response["success"]
    assert response["result"]["agent_errors"] == {}
    assert response["result"]["backups"] == [TEST_AGENT_BACKUP_RESULT]
    assert [tuple(mock_call) for mock_call in mock_api.mock_calls] == snapshot


async def test_agents_list_backups_fail(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    mock_api: MagicMock,
) -> None:
    """Test agent list backups fails."""
    mock_api.list_files = AsyncMock(side_effect=GoogleDriveApiError("some error"))

    client = await hass_ws_client(hass)
    await client.send_json_auto_id({"type": "backup/info"})
    response = await client.receive_json()

    assert response["success"]
    assert response["result"]["backups"] == []
    assert response["result"]["agent_errors"] == {
        TEST_AGENT_ID: "Failed to list backups: some error"
    }


@pytest.mark.parametrize(
    ("backup_id", "expected_result"),
    [
        (TEST_AGENT_BACKUP.backup_id, TEST_AGENT_BACKUP_RESULT),
        ("12345", None),
    ],
    ids=["found", "not_found"],
)
async def test_agents_get_backup(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    mock_api: MagicMock,
    backup_id: str,
    expected_result: dict[str, Any] | None,
) -> None:
    """Test agent get backup."""
    mock_api.list_files = AsyncMock(
        return_value={
            "files": [{"description": json.dumps(TEST_AGENT_BACKUP.as_dict())}]
        }
    )
    client = await hass_ws_client(hass)
    await client.send_json_auto_id({"type": "backup/details", "backup_id": backup_id})
    response = await client.receive_json()

    assert response["success"]
    assert response["result"]["agent_errors"] == {}
    assert response["result"]["backup"] == expected_result


async def test_agents_download(
    hass: HomeAssistant,
    hass_client: ClientSessionGenerator,
    mock_api: MagicMock,
    snapshot: SnapshotAssertion,
) -> None:
    """Test agent download backup."""
    mock_api.list_files = AsyncMock(
        side_effect=[
            {"files": [{"description": json.dumps(TEST_AGENT_BACKUP.as_dict())}]},
            {"files": [{"id": "backup-file-id"}]},
        ]
    )
    mock_response = AsyncMock(spec=ClientResponse)
    mock_response.content = mock_stream(b"backup data")
    mock_api.get_file_content = AsyncMock(return_value=mock_response)

    client = await hass_client()
    resp = await client.get(
        f"/api/backup/download/{TEST_AGENT_BACKUP.backup_id}?agent_id={TEST_AGENT_ID}"
    )
    assert resp.status == 200
    assert await resp.content.read() == b"backup data"

    assert [tuple(mock_call) for mock_call in mock_api.mock_calls] == snapshot


async def test_agents_download_fail(
    hass: HomeAssistant,
    hass_client: ClientSessionGenerator,
    mock_api: MagicMock,
) -> None:
    """Test agent download backup fails."""
    mock_api.list_files = AsyncMock(
        side_effect=[
            {"files": [{"description": json.dumps(TEST_AGENT_BACKUP.as_dict())}]},
            {"files": [{"id": "backup-file-id"}]},
        ]
    )
    mock_response = AsyncMock(spec=ClientResponse)
    mock_response.content = mock_stream(b"backup data")
    mock_api.get_file_content = AsyncMock(side_effect=GoogleDriveApiError("some error"))

    client = await hass_client()
    resp = await client.get(
        f"/api/backup/download/{TEST_AGENT_BACKUP.backup_id}?agent_id={TEST_AGENT_ID}"
    )
    assert resp.status == 500
    content = await resp.content.read()
    assert "Failed to download backup" in content.decode()


async def test_agents_download_file_not_found(
    hass: HomeAssistant,
    hass_client: ClientSessionGenerator,
    mock_api: MagicMock,
) -> None:
    """Test agent download backup raises error if not found."""
    mock_api.list_files = AsyncMock(
        side_effect=[
            {"files": [{"description": json.dumps(TEST_AGENT_BACKUP.as_dict())}]},
            {"files": []},
        ]
    )

    client = await hass_client()
    resp = await client.get(
        f"/api/backup/download/{TEST_AGENT_BACKUP.backup_id}?agent_id={TEST_AGENT_ID}"
    )
    assert resp.status == 404
    content = await resp.content.read()
    assert content == b""


async def test_agents_download_metadata_not_found(
    hass: HomeAssistant,
    hass_client: ClientSessionGenerator,
    mock_api: MagicMock,
) -> None:
    """Test agent download backup raises error if not found."""
    mock_api.list_files = AsyncMock(
        return_value={
            "files": [{"description": json.dumps(TEST_AGENT_BACKUP.as_dict())}]
        }
    )

    client = await hass_client()
    backup_id = "1234"
    assert backup_id != TEST_AGENT_BACKUP.backup_id

    resp = await client.get(
        f"/api/backup/download/{backup_id}?agent_id={TEST_AGENT_ID}"
    )
    assert resp.status == 404
    assert await resp.content.read() == b""


async def test_agents_upload(
    hass: HomeAssistant,
    hass_client: ClientSessionGenerator,
    caplog: pytest.LogCaptureFixture,
    mock_api: MagicMock,
    snapshot: SnapshotAssertion,
) -> None:
    """Test agent upload backup."""
    mock_api.resumable_upload_file = AsyncMock(return_value=None)

    client = await hass_client()

    with (
        patch(
            "homeassistant.components.backup.manager.BackupManager.async_get_backup",
        ) as fetch_backup,
        patch(
            "homeassistant.components.backup.manager.read_backup",
            return_value=TEST_AGENT_BACKUP,
        ),
        patch("pathlib.Path.open") as mocked_open,
    ):
        mocked_open.return_value.read = Mock(side_effect=[b"test", b""])
        fetch_backup.return_value = TEST_AGENT_BACKUP
        resp = await client.post(
            f"/api/backup/upload?agent_id={TEST_AGENT_ID}",
            data={"file": StringIO("test")},
        )

    assert resp.status == 201
    assert f"Uploading backup: {TEST_AGENT_BACKUP.backup_id}" in caplog.text
    assert f"Uploaded backup: {TEST_AGENT_BACKUP.backup_id}" in caplog.text

    mock_api.resumable_upload_file.assert_called_once()
    assert [tuple(mock_call) for mock_call in mock_api.mock_calls] == snapshot


async def test_agents_upload_create_folder_if_missing(
    hass: HomeAssistant,
    hass_client: ClientSessionGenerator,
    caplog: pytest.LogCaptureFixture,
    mock_api: MagicMock,
    snapshot: SnapshotAssertion,
) -> None:
    """Test agent upload backup creates folder if missing."""
    mock_api.list_files = AsyncMock(return_value={"files": []})
    mock_api.create_file = AsyncMock(
        return_value={"id": "new folder id", "name": "Home Assistant"}
    )
    mock_api.resumable_upload_file = AsyncMock(return_value=None)

    client = await hass_client()

    with (
        patch(
            "homeassistant.components.backup.manager.BackupManager.async_get_backup",
        ) as fetch_backup,
        patch(
            "homeassistant.components.backup.manager.read_backup",
            return_value=TEST_AGENT_BACKUP,
        ),
        patch("pathlib.Path.open") as mocked_open,
    ):
        mocked_open.return_value.read = Mock(side_effect=[b"test", b""])
        fetch_backup.return_value = TEST_AGENT_BACKUP
        resp = await client.post(
            f"/api/backup/upload?agent_id={TEST_AGENT_ID}",
            data={"file": StringIO("test")},
        )

    assert resp.status == 201
    assert f"Uploading backup: {TEST_AGENT_BACKUP.backup_id}" in caplog.text
    assert f"Uploaded backup: {TEST_AGENT_BACKUP.backup_id}" in caplog.text

    mock_api.create_file.assert_called_once()
    mock_api.resumable_upload_file.assert_called_once()
    assert [tuple(mock_call) for mock_call in mock_api.mock_calls] == snapshot


async def test_agents_upload_fail(
    hass: HomeAssistant,
    hass_client: ClientSessionGenerator,
    caplog: pytest.LogCaptureFixture,
    mock_api: MagicMock,
) -> None:
    """Test agent upload backup fails."""
    mock_api.resumable_upload_file = AsyncMock(
        side_effect=GoogleDriveApiError("some error")
    )
    mock_api.list_files = AsyncMock(
        side_effect=[
            {"files": [{"id": "HA folder ID", "name": "HA folder name"}]},
            {"files": []},
            {"files": [{"id": "HA folder ID", "name": "HA folder name"}]},
        ]
    )

    client = await hass_client()

    with (
        patch(
            "homeassistant.components.backup.manager.BackupManager.async_get_backup",
        ) as fetch_backup,
        patch(
            "homeassistant.components.backup.manager.read_backup",
            return_value=TEST_AGENT_BACKUP,
        ),
        patch("pathlib.Path.open") as mocked_open,
    ):
        mocked_open.return_value.read = Mock(side_effect=[b"test", b""])
        fetch_backup.return_value = TEST_AGENT_BACKUP
        resp = await client.post(
            f"/api/backup/upload?agent_id={TEST_AGENT_ID}",
            data={"file": StringIO("test")},
        )
        await hass.async_block_till_done()

    assert resp.status == 201
    assert "Failed to upload backup: some error" in caplog.text


async def test_agents_delete(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    mock_api: MagicMock,
    snapshot: SnapshotAssertion,
) -> None:
    """Test agent delete backup."""
    mock_api.list_files = AsyncMock(return_value={"files": [{"id": "backup-file-id"}]})
    mock_api.delete_file = AsyncMock(return_value=None)

    client = await hass_ws_client(hass)
    await client.send_json_auto_id(
        {
            "type": "backup/delete",
            "backup_id": TEST_AGENT_BACKUP.backup_id,
        }
    )
    response = await client.receive_json()

    assert response["success"]
    assert response["result"] == {"agent_errors": {}}

    mock_api.delete_file.assert_called_once()
    assert [tuple(mock_call) for mock_call in mock_api.mock_calls] == snapshot


async def test_agents_delete_fail(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    mock_api: MagicMock,
) -> None:
    """Test agent delete backup fails."""
    mock_api.list_files = AsyncMock(return_value={"files": [{"id": "backup-file-id"}]})
    mock_api.delete_file = AsyncMock(side_effect=GoogleDriveApiError("some error"))

    client = await hass_ws_client(hass)
    await client.send_json_auto_id(
        {
            "type": "backup/delete",
            "backup_id": TEST_AGENT_BACKUP.backup_id,
        }
    )
    response = await client.receive_json()

    assert response["success"]
    assert response["result"] == {
        "agent_errors": {TEST_AGENT_ID: "Failed to delete backup: some error"}
    }


async def test_agents_delete_not_found(
    hass: HomeAssistant,
    hass_ws_client: WebSocketGenerator,
    mock_api: MagicMock,
) -> None:
    """Test agent delete backup not found."""
    mock_api.list_files = AsyncMock(return_value={"files": []})

    client = await hass_ws_client(hass)
    backup_id = "1234"

    await client.send_json_auto_id(
        {
            "type": "backup/delete",
            "backup_id": backup_id,
        }
    )
    response = await client.receive_json()

    assert response["success"]
    assert response["result"] == {"agent_errors": {}}

    mock_api.delete_file.assert_not_called()
