"""Tests for the Growatt Server integration."""

from datetime import timedelta
import json

from freezegun.api import FrozenDateTimeFactory
import growattServer
from growattServer import GrowattV1ApiErrorCode
import pytest
import requests
from syrupy.assertion import SnapshotAssertion

from homeassistant.components.growatt_server import _CACHED_APIS
from homeassistant.components.growatt_server.const import (
    AUTH_API_TOKEN,
    AUTH_PASSWORD,
    CONF_AUTH_TYPE,
    CONF_PLANT_ID,
    DEFAULT_PLANT_ID,
    DEVICE_SCAN_INTERVAL,
    DOMAIN,
    LOGIN_INVALID_AUTH_CODE,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
    CONF_NAME,
    CONF_PASSWORD,
    CONF_TOKEN,
    CONF_URL,
    CONF_USERNAME,
    STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er

from . import setup_integration

from tests.common import MockConfigEntry, async_fire_time_changed


@pytest.mark.usefixtures("init_integration")
async def test_load_unload_config_entry(
    hass: HomeAssistant,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test loading and unloading the integration."""
    assert mock_config_entry.state is ConfigEntryState.LOADED

    await hass.config_entries.async_unload(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    assert mock_config_entry.state is ConfigEntryState.NOT_LOADED


@pytest.mark.usefixtures("init_integration")
async def test_device_info(
    snapshot: SnapshotAssertion,
    device_registry: dr.DeviceRegistry,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test device registry integration."""
    device_entry = device_registry.async_get_device_by_identifier(
        (DOMAIN, "MIN123456"), mock_config_entry.entry_id
    )
    assert device_entry is not None
    assert device_entry == snapshot


@pytest.mark.parametrize(
    ("exception", "expected_state"),
    [
        (
            growattServer.GrowattV1ApiError(
                message="API Error",
                error_code=GrowattV1ApiErrorCode.WRONG_DOMAIN,
                error_msg="Invalid JSON",
            ),
            ConfigEntryState.SETUP_ERROR,
        ),
        (
            json.decoder.JSONDecodeError("Invalid JSON", "", 0),
            ConfigEntryState.SETUP_ERROR,
        ),
        (
            growattServer.GrowattV1ApiError(
                message="Rate limited",
                error_code=GrowattV1ApiErrorCode.RATE_LIMITED,
                error_msg="Access frequency limit",
            ),
            ConfigEntryState.SETUP_RETRY,
        ),
    ],
)
async def test_setup_error_on_api_failure(
    hass: HomeAssistant,
    mock_growatt_v1_api,
    mock_config_entry: MockConfigEntry,
    exception: Exception,
    expected_state: ConfigEntryState,
) -> None:
    """Test setup error on API failures during device list."""
    mock_growatt_v1_api.device_list.side_effect = exception

    await setup_integration(hass, mock_config_entry)

    assert mock_config_entry.state is expected_state


@pytest.mark.usefixtures("init_integration")
async def test_coordinator_update_failed(
    hass: HomeAssistant,
    mock_growatt_v1_api,
    mock_config_entry: MockConfigEntry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test coordinator handles update failures gracefully."""
    # Integration should be loaded
    assert mock_config_entry.state is ConfigEntryState.LOADED

    # Cause coordinator update to fail
    mock_growatt_v1_api.min_detail.side_effect = growattServer.GrowattV1ApiError(
        message="Rate limited",
        error_code=growattServer.GrowattV1ApiErrorCode.RATE_LIMITED,
        error_msg="Too many requests",
    )

    # Trigger coordinator refresh
    freezer.tick(timedelta(minutes=5))
    async_fire_time_changed(hass)
    await hass.async_block_till_done(wait_background_tasks=True)

    # Integration should remain loaded despite coordinator error
    assert mock_config_entry.state is ConfigEntryState.LOADED


@pytest.mark.usefixtures("init_integration")
async def test_coordinator_update_json_error(
    hass: HomeAssistant,
    mock_growatt_v1_api,
    mock_config_entry: MockConfigEntry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test coordinator handles JSONDecodeError gracefully."""
    assert mock_config_entry.state is ConfigEntryState.LOADED

    mock_growatt_v1_api.min_detail.side_effect = json.decoder.JSONDecodeError(
        "Invalid JSON", "", 0
    )

    freezer.tick(timedelta(minutes=5))
    async_fire_time_changed(hass)
    await hass.async_block_till_done(wait_background_tasks=True)

    assert mock_config_entry.state is ConfigEntryState.LOADED
    assert (
        hass.states.get("switch.min123456_charge_from_grid").state == STATE_UNAVAILABLE
    )


@pytest.mark.usefixtures("init_integration")
async def test_coordinator_total_non_auth_api_error(
    hass: HomeAssistant,
    mock_growatt_v1_api,
    mock_config_entry: MockConfigEntry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test total coordinator handles non-auth V1 API errors as UpdateFailed."""
    assert mock_config_entry.state is ConfigEntryState.LOADED

    error = growattServer.GrowattV1ApiError(
        message="Rate limited",
        error_code=growattServer.GrowattV1ApiErrorCode.RATE_LIMITED,
        error_msg="Too many requests",
    )
    mock_growatt_v1_api.plant_energy_overview.side_effect = error

    freezer.tick(timedelta(minutes=5))
    async_fire_time_changed(hass)
    await hass.async_block_till_done(wait_background_tasks=True)

    # Should stay loaded (UpdateFailed is transient), no reauth flow started
    assert mock_config_entry.state is ConfigEntryState.LOADED
    flows = hass.config_entries.flow.async_progress()
    assert not any(flow["context"]["source"] == "reauth" for flow in flows)


async def test_setup_auth_failed_on_permission_denied(
    hass: HomeAssistant,
    mock_growatt_v1_api,
    mock_config_entry: MockConfigEntry,
) -> None:
    """Test error 10011 from device_list triggers reauth during setup."""
    error = growattServer.GrowattV1ApiError(
        message="Permission denied",
        error_code=growattServer.GrowattV1ApiErrorCode.NO_PRIVILEGE,
        error_msg="dummy error",
    )
    mock_growatt_v1_api.device_list.side_effect = error

    await setup_integration(hass, mock_config_entry)

    assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
    # Verify a reauth flow was started
    flows = hass.config_entries.flow.async_progress()
    assert any(
        flow["context"]["source"] == "reauth"
        and flow["context"]["entry_id"] == mock_config_entry.entry_id
        for flow in flows
    )


async def test_coordinator_auth_failed_triggers_reauth(
    hass: HomeAssistant,
    mock_growatt_v1_api,
    mock_config_entry: MockConfigEntry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test that error 10011 (no privilege) from coordinator update triggers reauth."""
    await setup_integration(hass, mock_config_entry)
    assert mock_config_entry.state is ConfigEntryState.LOADED

    error = growattServer.GrowattV1ApiError(
        message="Permission denied",
        error_code=growattServer.GrowattV1ApiErrorCode.NO_PRIVILEGE,
        error_msg="dummy error",
    )
    mock_growatt_v1_api.min_detail.side_effect = error

    freezer.tick(timedelta(minutes=5))
    async_fire_time_changed(hass)
    await hass.async_block_till_done(wait_background_tasks=True)

    # Verify a reauth flow was started
    flows = hass.config_entries.flow.async_progress()
    assert any(
        flow["context"]["source"] == "reauth"
        and flow["context"]["entry_id"] == mock_config_entry.entry_id
        for flow in flows
    )
    assert (
        hass.states.get("switch.min123456_charge_from_grid").state == STATE_UNAVAILABLE
    )


async def test_classic_api_coordinator_auth_failed_triggers_reauth(
    hass: HomeAssistant,
    mock_growatt_classic_api,
    mock_config_entry_classic: MockConfigEntry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test invalid classic API credentials trigger reauth on update."""
    mock_growatt_classic_api.device_list.return_value = [
        {"deviceSn": "TLX123456", "deviceType": "tlx"}
    ]
    mock_growatt_classic_api.plant_info.return_value = {
        "deviceList": [],
        "totalEnergy": 1250.0,
        "todayEnergy": 12.5,
        "invTodayPpv": 2500,
        "plantMoneyText": "123.45/USD",
    }
    mock_growatt_classic_api.tlx_detail.return_value = {
        "data": {"deviceSn": "TLX123456"}
    }

    await setup_integration(hass, mock_config_entry_classic)
    assert mock_config_entry_classic.state is ConfigEntryState.LOADED

    # Credentials expire between updates
    mock_growatt_classic_api.login.return_value = {
        "success": False,
        "msg": LOGIN_INVALID_AUTH_CODE,
    }

    freezer.tick(timedelta(minutes=5))
    async_fire_time_changed(hass)
    await hass.async_block_till_done(wait_background_tasks=True)

    flows = hass.config_entries.flow.async_progress()
    assert any(
        flow["context"]["source"] == "reauth"
        and flow["context"]["entry_id"] == mock_config_entry_classic.entry_id
        for flow in flows
    )
    assert hass.states.get("sensor.tlx123456_output_power").state == STATE_UNAVAILABLE


async def test_classic_api_setup(
    hass: HomeAssistant,
    snapshot: SnapshotAssertion,
    mock_growatt_classic_api,
    mock_config_entry_classic: MockConfigEntry,
    device_registry: dr.DeviceRegistry,
) -> None:
    """Test integration setup with Classic API (password auth)."""
    # Classic API doesn't support MIN devices - use TLX device instead
    mock_growatt_classic_api.device_list.return_value = [
        {"deviceSn": "TLX123456", "deviceType": "tlx"}
    ]

    await setup_integration(hass, mock_config_entry_classic)

    assert mock_config_entry_classic.state is ConfigEntryState.LOADED

    # Verify Classic API login was called
    mock_growatt_classic_api.login.assert_called()

    # Verify device was created
    device_entry = device_registry.async_get_device_by_identifier(
        (DOMAIN, "TLX123456"), mock_config_entry_classic.entry_id
    )
    assert device_entry is not None
    assert device_entry == snapshot


@pytest.mark.parametrize(
    ("config_data", "expected_auth_type"),
    [
        (
            {
                CONF_TOKEN: "test_token_123",
                CONF_URL: "https://openapi.growatt.com/",
                "plant_id": "plant_123",
            },
            AUTH_API_TOKEN,
        ),
        (
            {
                CONF_USERNAME: "test_user",
                CONF_PASSWORD: "test_password",
                CONF_URL: "https://server.growatt.com/",
                "plant_id": "plant_456",
            },
            AUTH_PASSWORD,
        ),
    ],
)
@pytest.mark.usefixtures("mock_growatt_v1_api", "mock_growatt_classic_api")
async def test_migrate_config_without_auth_type(
    hass: HomeAssistant,
    config_data: dict[str, str],
    expected_auth_type: str,
) -> None:
    """Test migration adds auth_type field to legacy configs and bumps version.

    This test verifies that config entries created before auth_type was introduced
    are properly migrated by:
    - Adding CONF_AUTH_TYPE with the correct value (AUTH_API_TOKEN or AUTH_PASSWORD)
    - Bumping version from 1.0 to 1.1
    """
    mock_config_entry = MockConfigEntry(
        domain=DOMAIN,
        data=config_data,
        unique_id=config_data["plant_id"],
        version=1,
        minor_version=0,
    )

    mock_config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    # Verify version was updated to 1.1
    assert mock_config_entry.version == 1
    assert mock_config_entry.minor_version == 1

    # Verify auth_type field was added during migration
    assert mock_config_entry.data[CONF_AUTH_TYPE] == expected_auth_type

    # Verify setup completed successfully after migration
    assert mock_config_entry.state is ConfigEntryState.LOADED


async def test_migrate_legacy_config_no_auth_fields(
    hass: HomeAssistant,
) -> None:
    """Test migration succeeds but setup fails for config without auth fields."""
    # Create a config entry without any auth fields
    invalid_config = {
        CONF_URL: "https://openapi.growatt.com/",
        "plant_id": "plant_789",
    }
    mock_config_entry = MockConfigEntry(
        domain=DOMAIN,
        data=invalid_config,
        unique_id="plant_789",
        version=1,
        minor_version=0,
    )

    mock_config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    # Migration succeeds (version bumped) but setup fails due to missing auth fields
    assert mock_config_entry.version == 1
    assert mock_config_entry.minor_version == 1
    assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR


@pytest.mark.parametrize(
    "exception",
    [
        requests.exceptions.RequestException("Connection error"),
        json.decoder.JSONDecodeError("Invalid JSON", "", 0),
    ],
    ids=["network_error", "json_error"],
)
async def test_classic_api_login_exceptions(
    hass: HomeAssistant,
    mock_growatt_classic_api,
    mock_config_entry_classic: MockConfigEntry,
    exception: Exception,
) -> None:
    """Test Classic API setup with login exceptions."""
    mock_growatt_classic_api.login.side_effect = exception

    await setup_integration(hass, mock_config_entry_classic)

    assert mock_config_entry_classic.state is ConfigEntryState.SETUP_ERROR


@pytest.mark.parametrize(
    "login_response",
    [
        {"success": False, "msg": "502"},
        {"success": False, "msg": "Server maintenance"},
    ],
    ids=["invalid_auth", "other_login_error"],
)
async def test_classic_api_login_failures(
    hass: HomeAssistant,
    mock_growatt_classic_api,
    mock_config_entry_classic: MockConfigEntry,
    login_response: dict,
) -> None:
    """Test Classic API setup with login failures."""
    mock_growatt_classic_api.login.return_value = login_response

    await setup_integration(hass, mock_config_entry_classic)

    assert mock_config_entry_classic.state is ConfigEntryState.SETUP_ERROR


@pytest.mark.parametrize(
    "exception",
    [
        requests.exceptions.RequestException("Connection error"),
        json.decoder.JSONDecodeError("Invalid JSON", "", 0),
    ],
    ids=["network_error", "json_error"],
)
async def test_classic_api_device_list_exceptions(
    hass: HomeAssistant,
    mock_growatt_classic_api,
    exception: Exception,
) -> None:
    """Test Classic API setup with device_list exceptions."""
    # Create a config entry that won't trigger migration
    mock_config_entry = MockConfigEntry(
        domain=DOMAIN,
        data={
            CONF_AUTH_TYPE: AUTH_PASSWORD,
            CONF_USERNAME: "test_user",
            CONF_PASSWORD: "test_password",
            CONF_URL: "https://server.growatt.com/",
            CONF_PLANT_ID: "specific_plant_123",  # Specific ID to avoid migration
        },
        unique_id="plant_123",
    )

    # device_list raises exception during setup
    mock_growatt_classic_api.device_list.side_effect = exception

    await setup_integration(hass, mock_config_entry)

    assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR


async def test_classic_api_device_list_no_devices(
    hass: HomeAssistant,
    mock_growatt_classic_api,
) -> None:
    """Test Classic API setup when device list returns no devices."""
    # Create a config entry that won't trigger migration
    mock_config_entry = MockConfigEntry(
        domain=DOMAIN,
        data={
            CONF_AUTH_TYPE: AUTH_PASSWORD,
            CONF_USERNAME: "test_user",
            CONF_PASSWORD: "test_password",
            CONF_URL: "https://server.growatt.com/",
            CONF_PLANT_ID: "specific_plant_456",  # Specific ID to avoid migration
        },
        unique_id="plant_456",
    )

    # device_list returns empty list (no devices)
    mock_growatt_classic_api.device_list.return_value = []

    await setup_integration(hass, mock_config_entry)

    # Should still load successfully even with no devices
    assert mock_config_entry.state is ConfigEntryState.LOADED


@pytest.mark.parametrize(
    "exception",
    [
        requests.exceptions.RequestException("Connection error"),
        json.decoder.JSONDecodeError("Invalid JSON", "", 0),
    ],
    ids=["network_error", "json_error"],
)
async def test_classic_api_device_list_errors(
    hass: HomeAssistant,
    mock_growatt_classic_api,
    mock_config_entry_classic: MockConfigEntry,
    exception: Exception,
) -> None:
    """Test Classic API setup with device list errors."""
    mock_growatt_classic_api.device_list.side_effect = exception

    await setup_integration(hass, mock_config_entry_classic)

    assert mock_config_entry_classic.state is ConfigEntryState.SETUP_ERROR


async def test_unknown_api_version(
    hass: HomeAssistant,
) -> None:
    """Test setup with unknown API version."""
    # Create a config entry with invalid auth type
    config = {
        CONF_URL: "https://openapi.growatt.com/",
        "plant_id": "plant_123",
        CONF_AUTH_TYPE: "unknown_auth",  # Invalid auth type
    }
    mock_config_entry = MockConfigEntry(
        domain=DOMAIN,
        data=config,
        unique_id="plant_123",
    )

    await setup_integration(hass, mock_config_entry)

    assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR


async def test_classic_api_auto_select_plant(
    hass: HomeAssistant,
    mock_growatt_classic_api,
    mock_config_entry_classic_default_plant: MockConfigEntry,
) -> None:
    """Test Classic API setup with default plant ID (auto-selects first plant)."""
    # Login succeeds and plant_list returns a plant
    mock_growatt_classic_api.login.return_value = {
        "success": True,
        "user": {"id": 123456},
    }
    mock_growatt_classic_api.plant_list.return_value = {
        "data": [{"plantId": "AUTO_PLANT_123", "plantName": "Auto Plant"}]
    }
    mock_growatt_classic_api.device_list.return_value = [
        {"deviceSn": "TLX999999", "deviceType": "tlx"}
    ]

    await setup_integration(hass, mock_config_entry_classic_default_plant)

    # Should be loaded successfully with auto-selected plant
    assert mock_config_entry_classic_default_plant.state is ConfigEntryState.LOADED


async def test_v1_api_unsupported_device_type(
    hass: HomeAssistant,
    mock_growatt_v1_api,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test V1 API logs warning for unsupported device types (non-MIN)."""
    config = {
        CONF_TOKEN: "test_token_123",
        CONF_URL: "https://openapi.growatt.com/",
        "plant_id": "plant_123",
        CONF_AUTH_TYPE: AUTH_API_TOKEN,
    }
    mock_config_entry = MockConfigEntry(
        domain=DOMAIN,
        data=config,
        unique_id="plant_123",
    )

    # Return mix of MIN (type 7) and other device types
    mock_growatt_v1_api.device_list.return_value = {
        "devices": [
            {"device_sn": "MIN123456", "type": 7},  # Supported (MIN)
            {"device_sn": "UNK999999", "type": 3},  # Unsupported
        ]
    }

    await setup_integration(hass, mock_config_entry)

    assert mock_config_entry.state is ConfigEntryState.LOADED
    # Verify warning was logged for unsupported device
    assert "Device UNK999999 with type 3 not supported in Open API V1" in caplog.text


async def test_migrate_version_bump(
    hass: HomeAssistant,
    mock_growatt_classic_api,
) -> None:
    """Test migration from 1.0 to 1.1 resolves DEFAULT_PLANT_ID and bumps version.

    This test verifies that:
    - Migration successfully resolves DEFAULT_PLANT_ID ("0") to actual plant_id
    - Config entry version is bumped from 1.0 to 1.1
    """
    # Create a version 1.0 config entry with DEFAULT_PLANT_ID
    mock_config_entry = MockConfigEntry(
        domain=DOMAIN,
        data={
            CONF_AUTH_TYPE: AUTH_PASSWORD,
            CONF_USERNAME: "test_user",
            CONF_PASSWORD: "test_password",
            CONF_URL: "https://server.growatt.com/",
            CONF_PLANT_ID: DEFAULT_PLANT_ID,
            CONF_NAME: "Test Plant",
        },
        unique_id="plant_default",
        version=1,
        minor_version=0,
    )

    # Mock successful API responses for migration and setup
    mock_growatt_classic_api.login.return_value = {
        "success": True,
        "user": {"id": 123456},
    }
    mock_growatt_classic_api.plant_list.return_value = {
        "data": [{"plantId": "RESOLVED_PLANT_789", "plantName": "My Plant"}]
    }
    mock_growatt_classic_api.device_list.return_value = [
        {"deviceSn": "TLX123456", "deviceType": "tlx"}
    ]

    mock_config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    # Verify version was updated to 1.1
    assert mock_config_entry.version == 1
    assert mock_config_entry.minor_version == 1

    # Verify plant_id was resolved to actual plant_id (not DEFAULT_PLANT_ID)
    assert mock_config_entry.data[CONF_PLANT_ID] == "RESOLVED_PLANT_789"

    # Verify setup completed successfully after migration
    assert mock_config_entry.state is ConfigEntryState.LOADED


async def test_setup_reuses_cached_api_from_migration(
    hass: HomeAssistant,
    mock_growatt_classic_api,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test that setup reuses cached API instance from migration.

    This test verifies the rate limit optimization where:
    1. Migration calls login() and caches the authenticated API instance
    2. Setup retrieves and reuses the cached API (avoiding a second login())
    3. The cached API is removed after use (one-time use pattern)

    Without this caching, we would call login() twice within seconds:
        Migration: login() → plant_list()
        Setup:     login() → device_list()
    This would trigger Growatt API rate limiting (5-minute window per endpoint).

    With caching, we only call login() once:
        Migration: login() → plant_list() → [cache API]
        Setup:     [reuse API] → device_list()
    """
    # Create a version 1.0 config entry with DEFAULT_PLANT_ID
    mock_config_entry = MockConfigEntry(
        domain=DOMAIN,
        data={
            CONF_AUTH_TYPE: AUTH_PASSWORD,
            CONF_USERNAME: "test_user",
            CONF_PASSWORD: "test_password",
            CONF_URL: "https://server.growatt.com/",
            CONF_PLANT_ID: DEFAULT_PLANT_ID,
            CONF_NAME: "Test Plant",
        },
        unique_id="plant_default",
        version=1,
        minor_version=0,
    )

    # Mock successful API responses
    mock_growatt_classic_api.login.return_value = {
        "success": True,
        "user": {"id": 123456},
    }
    mock_growatt_classic_api.plant_list.return_value = {
        "data": [{"plantId": "RESOLVED_PLANT_789", "plantName": "My Plant"}]
    }
    mock_growatt_classic_api.device_list.return_value = [
        {"deviceSn": "TLX123456", "deviceType": "tlx"}
    ]
    mock_growatt_classic_api.plant_info.return_value = {
        "deviceList": [],
        "totalEnergy": 1250.0,
        "todayEnergy": 12.5,
        "invTodayPpv": 2500,
        "plantMoneyText": "123.45/USD",
    }
    mock_growatt_classic_api.tlx_detail.return_value = {
        "data": {"deviceSn": "TLX123456"}
    }

    mock_config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    # Verify migration successfully resolved plant_id
    assert mock_config_entry.data[CONF_PLANT_ID] == "RESOLVED_PLANT_789"

    # Verify integration loaded successfully
    assert mock_config_entry.state is ConfigEntryState.LOADED

    # Verify log message confirms API reuse (rate limit optimization)
    assert "Reusing logged-in session from migration" in caplog.text

    # Verify login was called with correct credentials
    # Note: Coordinators also call login() during refresh, so we verify
    # the call was made but don't assert it was called exactly once
    mock_growatt_classic_api.login.assert_called_with("test_user", "test_password")

    # Verify plant_list was called only once (during migration, not during setup)
    # This confirms setup did NOT resolve plant_id again (optimization working)
    mock_growatt_classic_api.plant_list.assert_called_once_with(123456)

    # Verify the cached API was removed after use (one-time handoff)
    assert mock_config_entry.entry_id not in _CACHED_APIS


async def test_migrate_failure_returns_false(
    hass: HomeAssistant,
    mock_growatt_classic_api,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test migration returns False on API failure to allow retry.

    When migration fails due to API errors (network issues, etc.),
    it should return False and NOT bump the version. This allows Home Assistant
    to retry the migration on the next restart.
    """
    # Create a version 1.0 config entry with DEFAULT_PLANT_ID
    mock_config_entry = MockConfigEntry(
        domain=DOMAIN,
        data={
            CONF_AUTH_TYPE: AUTH_PASSWORD,
            CONF_USERNAME: "test_user",
            CONF_PASSWORD: "test_password",
            CONF_URL: "https://server.growatt.com/",
            CONF_PLANT_ID: DEFAULT_PLANT_ID,
            CONF_NAME: "Test Plant",
        },
        unique_id="plant_default",
        version=1,
        minor_version=0,
    )

    # Mock API failure (e.g., network error during login)
    mock_growatt_classic_api.login.side_effect = requests.exceptions.RequestException(
        "Network error"
    )

    mock_config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    # Verify migration failed (entry is in migration error state)
    assert mock_config_entry.state is ConfigEntryState.MIGRATION_ERROR

    # Verify version was NOT bumped (remains 1.0)
    assert mock_config_entry.version == 1
    assert mock_config_entry.minor_version == 0

    # Verify plant_id was NOT changed (remains DEFAULT_PLANT_ID)
    assert mock_config_entry.data[CONF_PLANT_ID] == DEFAULT_PLANT_ID

    # Verify error was logged
    assert "Failed to resolve plant_id during migration" in caplog.text
    assert "Migration will retry on next restart" in caplog.text


@pytest.mark.usefixtures("mock_growatt_classic_api")
async def test_migrate_already_migrated(
    hass: HomeAssistant,
) -> None:
    """Test migration is skipped for already migrated entries."""
    # Create a config entry already at version 1.1
    mock_config_entry = MockConfigEntry(
        domain=DOMAIN,
        data={
            CONF_AUTH_TYPE: AUTH_PASSWORD,
            CONF_USERNAME: "test_user",
            CONF_PASSWORD: "test_password",
            CONF_URL: "https://server.growatt.com/",
            CONF_PLANT_ID: "specific_plant_123",
        },
        unique_id="plant_specific",
        version=1,
        minor_version=1,
    )

    mock_config_entry.add_to_hass(hass)
    await hass.config_entries.async_setup(mock_config_entry.entry_id)
    await hass.async_block_till_done()

    # Verify version remains 1.1 (no change)
    assert mock_config_entry.version == 1
    assert mock_config_entry.minor_version == 1

    # Plant ID should remain unchanged
    assert mock_config_entry.data[CONF_PLANT_ID] == "specific_plant_123"

    # Verify setup completed successfully
    assert mock_config_entry.state is ConfigEntryState.LOADED


@pytest.mark.usefixtures("init_integration")
async def test_dynamic_device_added(
    hass: HomeAssistant,
    mock_growatt_v1_api,
    mock_config_entry: MockConfigEntry,
    device_registry: dr.DeviceRegistry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test that new devices are dynamically added when discovered during a scan."""
    # Initially only MIN123456 device exists
    assert (
        device_registry.async_get_device_by_identifier(
            (DOMAIN, "MIN123456"), mock_config_entry.entry_id
        )
        is not None
    )
    assert (
        device_registry.async_get_device_by_identifier(
            (DOMAIN, "NEW456789"), mock_config_entry.entry_id
        )
        is None
    )

    # Mock a new device appearing in the device list
    mock_growatt_v1_api.device_list.return_value = {
        "devices": [
            {"device_sn": "MIN123456", "type": 7},
            {"device_sn": "NEW456789", "type": 7},
        ]
    }
    mock_growatt_v1_api.min_detail.return_value = {
        "deviceSn": "NEW456789",
        "acChargeEnable": 0,
    }

    # Trigger the periodic device scan
    freezer.tick(DEVICE_SCAN_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done(wait_background_tasks=True)

    # New device should now be in the device registry
    assert (
        device_registry.async_get_device_by_identifier(
            (DOMAIN, "NEW456789"), mock_config_entry.entry_id
        )
        is not None
    )
    # New device should be in runtime_data
    assert "NEW456789" in mock_config_entry.runtime_data.devices
    # Entities for the new device should have been created via the dispatcher.
    # Verify multiple entity types to confirm end-to-end dynamic device support
    assert hass.states.get("switch.new456789_charge_from_grid") is not None
    # Additional check: verify entities exist in the entity registry
    entity_registry = er.async_get(hass)  # pylint: disable=home-assistant-tests-registry-fixtures
    new_device_entry = device_registry.async_get_device_by_identifier(
        (DOMAIN, "NEW456789"), mock_config_entry.entry_id
    )
    new_device_entities = er.async_entries_for_device(
        entity_registry, new_device_entry.id, include_disabled_entities=True
    )
    assert len(new_device_entities) > 0, "No entities created for new device"


@pytest.mark.usefixtures("init_integration")
async def test_stale_device_removed(
    hass: HomeAssistant,
    mock_growatt_v1_api,
    mock_config_entry: MockConfigEntry,
    device_registry: dr.DeviceRegistry,
    entity_registry: er.EntityRegistry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test that stale devices are removed from the device registry during a scan."""
    # Initially MIN123456 device exists with entities in the state machine
    assert (
        device_registry.async_get_device_by_identifier(
            (DOMAIN, "MIN123456"), mock_config_entry.entry_id
        )
        is not None
    )
    assert hass.states.get("switch.min123456_charge_from_grid") is not None

    # Mock the device disappearing from the API
    mock_growatt_v1_api.device_list.return_value = {"devices": []}

    # Trigger the periodic device scan
    freezer.tick(DEVICE_SCAN_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done(wait_background_tasks=True)

    # The device should be removed from HA
    assert (
        device_registry.async_get_device_by_identifier(
            (DOMAIN, "MIN123456"), mock_config_entry.entry_id
        )
        is None
    )
    # The coordinator should be removed from runtime_data
    assert "MIN123456" not in mock_config_entry.runtime_data.devices
    # Orphaned entities must also be gone from the entity registry and state machine
    assert entity_registry.async_get("switch.min123456_charge_from_grid") is None
    assert hass.states.get("switch.min123456_charge_from_grid") is None


@pytest.mark.usefixtures("init_integration")
async def test_device_scan_error_is_silent(
    hass: HomeAssistant,
    mock_growatt_v1_api,
    mock_config_entry: MockConfigEntry,
    device_registry: dr.DeviceRegistry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test that errors during device scan are handled gracefully without crashing."""
    assert mock_config_entry.state is ConfigEntryState.LOADED

    # Simulate a scan failure
    mock_growatt_v1_api.device_list.side_effect = growattServer.GrowattV1ApiError(
        message="Temporary error",
        error_code=growattServer.GrowattV1ApiErrorCode.RATE_LIMITED,
        error_msg="Temporary error",
    )

    freezer.tick(DEVICE_SCAN_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done(wait_background_tasks=True)

    # Integration should remain loaded - scan errors are non-fatal
    assert mock_config_entry.state is ConfigEntryState.LOADED
    # Existing device should still be present
    assert (
        device_registry.async_get_device_by_identifier(
            (DOMAIN, "MIN123456"), mock_config_entry.entry_id
        )
        is not None
    )


@pytest.mark.usefixtures("init_integration")
async def test_dynamic_device_refresh_fails_is_skipped(
    hass: HomeAssistant,
    mock_growatt_v1_api,
    mock_config_entry: MockConfigEntry,
    device_registry: dr.DeviceRegistry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test that a new device whose first coordinator refresh fails is not added."""
    # Mock a new device appearing but its data endpoint failing
    mock_growatt_v1_api.device_list.return_value = {
        "devices": [
            {"device_sn": "MIN123456", "type": 7},
            {"device_sn": "NEW456789", "type": 7},
        ]
    }

    def min_detail_side_effect(device_sn: str) -> dict:
        if device_sn == "NEW456789":
            raise growattServer.GrowattV1ApiError(
                message="Device not reachable",
                error_code=growattServer.GrowattV1ApiErrorCode.RATE_LIMITED,
                error_msg="Device not reachable",
            )
        return mock_growatt_v1_api.min_detail.return_value

    mock_growatt_v1_api.min_detail.side_effect = min_detail_side_effect

    freezer.tick(DEVICE_SCAN_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done(wait_background_tasks=True)

    # New device should NOT be added — its refresh failed
    assert (
        device_registry.async_get_device_by_identifier(
            (DOMAIN, "NEW456789"), mock_config_entry.entry_id
        )
        is None
    )
    assert "NEW456789" not in mock_config_entry.runtime_data.devices
    # Existing device must be unaffected
    assert (
        device_registry.async_get_device_by_identifier(
            (DOMAIN, "MIN123456"), mock_config_entry.entry_id
        )
        is not None
    )


async def test_classic_api_device_scan(
    hass: HomeAssistant,
    mock_growatt_classic_api,
    mock_config_entry_classic: MockConfigEntry,
    device_registry: dr.DeviceRegistry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test that the periodic device scan works correctly for Classic API (password auth).

    The scan must reuse the total coordinator's authenticated session (no extra login).
    """
    mock_growatt_classic_api.device_list.return_value = [
        {"deviceSn": "TLX123456", "deviceType": "tlx"}
    ]
    await setup_integration(hass, mock_config_entry_classic)
    assert mock_config_entry_classic.state is ConfigEntryState.LOADED

    # Mock a new device appearing; reset device_list call count after setup
    # so we can assert the scan calls it exactly once.
    mock_growatt_classic_api.device_list.reset_mock()
    mock_growatt_classic_api.device_list.return_value = [
        {"deviceSn": "TLX123456", "deviceType": "tlx"},
        {"deviceSn": "TLX999999", "deviceType": "tlx"},
    ]
    mock_growatt_classic_api.tlx_detail.return_value = {
        "data": {"deviceSn": "TLX999999"}
    }

    freezer.tick(DEVICE_SCAN_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done(wait_background_tasks=True)

    # New device should be added via the classic scan path
    assert (
        device_registry.async_get_device_by_identifier(
            (DOMAIN, "TLX999999"), mock_config_entry_classic.entry_id
        )
        is not None
    )
    assert "TLX999999" in mock_config_entry_classic.runtime_data.devices

    # The scan reuses the coordinator's API session — device_list must have been
    # called exactly once (by the scan itself, not a fresh login+list cycle).
    mock_growatt_classic_api.device_list.assert_called_once()


async def test_classic_api_stale_device_removed(
    hass: HomeAssistant,
    mock_growatt_classic_api,
    mock_config_entry_classic: MockConfigEntry,
    device_registry: dr.DeviceRegistry,
    entity_registry: er.EntityRegistry,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test that stale devices are removed during a Classic API device scan."""
    mock_growatt_classic_api.device_list.return_value = [
        {"deviceSn": "TLX123456", "deviceType": "tlx"}
    ]
    await setup_integration(hass, mock_config_entry_classic)
    assert mock_config_entry_classic.state is ConfigEntryState.LOADED

    # Verify device exists after setup
    assert (
        device_registry.async_get_device_by_identifier(
            (DOMAIN, "TLX123456"), mock_config_entry_classic.entry_id
        )
        is not None
    )
    assert "TLX123456" in mock_config_entry_classic.runtime_data.devices

    # Mock the device disappearing from the API
    mock_growatt_classic_api.device_list.return_value = []

    # Trigger the periodic device scan
    freezer.tick(DEVICE_SCAN_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done(wait_background_tasks=True)

    # The device should be removed from HA
    assert (
        device_registry.async_get_device_by_identifier(
            (DOMAIN, "TLX123456"), mock_config_entry_classic.entry_id
        )
        is None
    )
    # The coordinator should be removed from runtime_data
    assert "TLX123456" not in mock_config_entry_classic.runtime_data.devices
