"""Tests for M2-T01: GET /api/config and PUT /api/config.
   Tests for M2-T05: POST /api/config/smtp/test."""
from __future__ import annotations

import sqlite3
from unittest.mock import MagicMock, patch

from fastapi.testclient import TestClient

from app.config import get_settings
from app.services.config_page import CONFIG_FIELDS
from app.services.email import EmailConfigurationError, EmailDeliveryError


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _login(client: TestClient) -> None:
    """Log in as admin/test-password using the JSON API."""
    resp = client.post(
        "/api/auth/login",
        json={"username": "admin", "password": "test-password"},
    )
    assert resp.status_code == 200, f"Login failed: {resp.status_code}"


def _stringify(value) -> str:
    if value is None:
        return ""
    if isinstance(value, bool):
        return str(value).lower()
    return str(value)


def _full_config_payload(overrides: dict[str, str] | None = None) -> dict[str, str]:
    """Build a complete env_name→value dict mirroring the Jinja form's full submission.

    Secrets default to "" (keep-old semantics). Non-secrets use current settings defaults.
    """
    settings = get_settings()
    payload: dict[str, str] = {}
    for field in CONFIG_FIELDS:
        if field.secret:
            payload[field.env_name] = ""  # blank → keep existing
        else:
            payload[field.env_name] = _stringify(getattr(settings, field.setting_attr))
    if overrides:
        payload.update(overrides)
    return payload


# ---------------------------------------------------------------------------
# GET /api/config — unauthenticated
# ---------------------------------------------------------------------------

def test_get_config_unauthenticated_returns_401(client: TestClient) -> None:
    response = client.get("/api/config")
    assert response.status_code == 401


# ---------------------------------------------------------------------------
# GET /api/config — authenticated
# ---------------------------------------------------------------------------

def test_get_config_authenticated_returns_sections(client: TestClient) -> None:
    _login(client)

    response = client.get("/api/config")

    assert response.status_code == 200
    body = response.json()
    assert "sections" in body
    assert isinstance(body["sections"], list)
    assert len(body["sections"]) > 0


def test_get_config_sections_have_expected_structure(client: TestClient) -> None:
    _login(client)

    response = client.get("/api/config")
    body = response.json()

    for section in body["sections"]:
        assert "name" in section
        assert "fields" in section
        assert isinstance(section["fields"], list)
        for field in section["fields"]:
            assert "env_name" in field
            assert "label" in field
            assert "value" in field
            assert "secret" in field
            assert "input_type" in field
            assert "configured" in field


def test_get_config_secret_fields_have_empty_string_value(client: TestClient) -> None:
    _login(client)

    response = client.get("/api/config")
    body = response.json()

    for section in body["sections"]:
        for field in section["fields"]:
            if field["secret"]:
                assert field["value"] == "", (
                    f"Secret field {field['env_name']} should be masked (empty string), "
                    f"got {field['value']!r}"
                )


def test_get_config_includes_known_sections(client: TestClient) -> None:
    _login(client)

    response = client.get("/api/config")
    body = response.json()

    section_names = {s["name"] for s in body["sections"]}
    assert "System" in section_names
    assert "SMTP" in section_names
    assert "Authentication" in section_names


# ---------------------------------------------------------------------------
# PUT /api/config — unauthenticated
# ---------------------------------------------------------------------------

def test_put_config_unauthenticated_returns_401(client: TestClient) -> None:
    response = client.put(
        "/api/config",
        json={"updates": _full_config_payload()},
        headers={"X-CSRF-Token": "any-token"},
    )
    assert response.status_code == 401


# ---------------------------------------------------------------------------
# PUT /api/config — authenticated but missing CSRF
# ---------------------------------------------------------------------------

def test_put_config_authenticated_missing_csrf_returns_403(client: TestClient) -> None:
    _login(client)

    # No X-CSRF-Token header at all
    response = client.put(
        "/api/config",
        json={"updates": _full_config_payload()},
    )
    assert response.status_code == 403


def test_put_config_authenticated_empty_csrf_returns_403(client: TestClient) -> None:
    _login(client)

    # Empty string X-CSRF-Token header counts as missing
    response = client.put(
        "/api/config",
        json={"updates": _full_config_payload()},
        headers={"X-CSRF-Token": ""},
    )
    assert response.status_code == 403


# ---------------------------------------------------------------------------
# PUT /api/config — authenticated + CSRF present (any non-empty value)
# ---------------------------------------------------------------------------

def test_put_config_with_csrf_header_updates_app_name(
    client: TestClient, test_database_urls
) -> None:
    _login(client)

    payload = _full_config_payload({"APP_NAME": "Updated via API"})
    response = client.put(
        "/api/config",
        json={"updates": payload},
        headers={"X-CSRF-Token": "any-non-empty-value"},
    )

    assert response.status_code == 200
    body = response.json()
    assert "sections" in body

    # The refreshed config in the response should reflect the new name
    system_section = next(s for s in body["sections"] if s["name"] == "System")
    app_name_field = next(f for f in system_section["fields"] if f["env_name"] == "APP_NAME")
    assert app_name_field["value"] == "Updated via API"


def test_put_config_reapplies_dsmr_subscription(
    client: TestClient, test_database_urls
) -> None:
    """Saving config must re-apply the DSMR subscription so enabling DSMR ingest
    takes effect without an app restart (the route calls apply_dsmr_subscription
    with the refreshed settings)."""
    _login(client)

    payload = _full_config_payload({"DSMR_INGEST_ENABLED": "true"})
    with patch("app.services.dsmr_ingest.apply_dsmr_subscription") as spy:
        response = client.put(
            "/api/config",
            json={"updates": payload},
            headers={"X-CSRF-Token": "any-non-empty-value"},
        )

    assert response.status_code == 200
    spy.assert_called_once()
    applied_settings = spy.call_args.args[0]
    assert applied_settings.dsmr_ingest_enabled is True


def test_put_config_blank_secret_keeps_existing_value(
    client: TestClient, test_database_urls
) -> None:
    """Submitting a blank value for a secret field must NOT overwrite the stored secret."""
    _login(client)

    # First: store a secret via a full PUT with the secret value set
    payload_with_secret = _full_config_payload({"SMTP_PASSWORD": "original-secret"})
    resp1 = client.put(
        "/api/config",
        json={"updates": payload_with_secret},
        headers={"X-CSRF-Token": "token"},
    )
    assert resp1.status_code == 200, f"First PUT failed: {resp1.status_code} {resp1.text}"

    # Second: PUT with blank for that secret (keep-old semantics)
    payload_blank_secret = _full_config_payload({"SMTP_PASSWORD": ""})
    resp2 = client.put(
        "/api/config",
        json={"updates": payload_blank_secret},
        headers={"X-CSRF-Token": "token"},
    )
    assert resp2.status_code == 200, f"Second PUT failed: {resp2.status_code} {resp2.text}"

    # The stored value in the DB should still be the original secret
    conn = sqlite3.connect(test_database_urls["app_path"])
    try:
        rows = dict(conn.execute("SELECT key, value FROM app_config").fetchall())
    finally:
        conn.close()

    assert rows.get("SMTP_PASSWORD") == "original-secret"


def test_put_config_returns_refreshed_sections(client: TestClient) -> None:
    _login(client)

    payload = _full_config_payload({"APP_NAME": "Refreshed Name"})
    response = client.put(
        "/api/config",
        json={"updates": payload},
        headers={"X-CSRF-Token": "token"},
    )

    assert response.status_code == 200
    body = response.json()
    assert "sections" in body
    assert isinstance(body["sections"], list)
    assert len(body["sections"]) > 0

    # Sections should reflect updated value
    system_section = next(s for s in body["sections"] if s["name"] == "System")
    app_name_field = next(f for f in system_section["fields"] if f["env_name"] == "APP_NAME")
    assert app_name_field["value"] == "Refreshed Name"


def test_put_config_invalid_value_returns_422_and_does_not_write(
    client: TestClient, test_database_urls
) -> None:
    """An invalid config value (e.g. bad type for a typed field) must return 4xx and not persist."""
    _login(client)

    # SMTP_PORT expects an integer; submit something that fails Settings validation
    payload = _full_config_payload({"SMTP_PORT": "not-a-number"})
    response = client.put(
        "/api/config",
        json={"updates": payload},
        headers={"X-CSRF-Token": "token"},
    )

    assert response.status_code == 422

    # Confirm the bad value was not persisted
    conn = sqlite3.connect(test_database_urls["app_path"])
    try:
        rows = dict(conn.execute("SELECT key, value FROM app_config").fetchall())
    finally:
        conn.close()

    assert rows.get("SMTP_PORT") != "not-a-number"


# ---------------------------------------------------------------------------
# Response schema correctness — secret values never leak in response
# ---------------------------------------------------------------------------

def test_put_config_response_does_not_leak_secret_values(client: TestClient) -> None:
    _login(client)

    # Set a secret
    payload1 = _full_config_payload({"HOME_ASSISTANT_AUTH_TOKEN": "super-secret-token"})
    resp1 = client.put(
        "/api/config",
        json={"updates": payload1},
        headers={"X-CSRF-Token": "token"},
    )
    assert resp1.status_code == 200

    # Do another PUT and check response doesn't leak the secret
    payload2 = _full_config_payload({"APP_NAME": "check-secrets"})
    response = client.put(
        "/api/config",
        json={"updates": payload2},
        headers={"X-CSRF-Token": "token"},
    )

    assert response.status_code == 200
    body = response.json()

    for section in body["sections"]:
        for field in section["fields"]:
            if field["secret"]:
                assert field["value"] == "", (
                    f"Secret field {field['env_name']} leaked in PUT response"
                )

    # The secret value itself should not appear anywhere in the raw response
    assert "super-secret-token" not in str(body)


# ---------------------------------------------------------------------------
# POST /api/config/smtp/test — M2-T05
# ---------------------------------------------------------------------------

_SMTP_TEST_URL = "/api/config/smtp/test"
_SMTP_SEND_PATH = "app.api.routes.api.config.send_smtp_test_email"


def test_smtp_test_unauthenticated_returns_401(client: TestClient) -> None:
    """Unauthenticated request must return 401 (require_session fires before require_csrf)."""
    response = client.post(
        _SMTP_TEST_URL,
        headers={"X-CSRF-Token": "any-token"},
    )
    assert response.status_code == 401


def test_smtp_test_authenticated_missing_csrf_returns_403(client: TestClient) -> None:
    """Authenticated but no X-CSRF-Token header must return 403."""
    _login(client)
    response = client.post(_SMTP_TEST_URL)
    assert response.status_code == 403


def test_smtp_test_authenticated_empty_csrf_returns_403(client: TestClient) -> None:
    """Authenticated but empty X-CSRF-Token header must return 403."""
    _login(client)
    response = client.post(_SMTP_TEST_URL, headers={"X-CSRF-Token": ""})
    assert response.status_code == 403


def test_smtp_test_success_returns_200(client: TestClient) -> None:
    """When send_smtp_test_email succeeds (returns None), endpoint returns 200 with result=success."""
    _login(client)

    with patch(_SMTP_SEND_PATH, return_value=None) as mock_send:
        response = client.post(_SMTP_TEST_URL, headers={"X-CSRF-Token": "any-token"})

    mock_send.assert_called_once()
    assert response.status_code == 200
    body = response.json()
    assert body["result"] == "success"
    assert "message" in body


def test_smtp_test_config_error_returns_400(client: TestClient) -> None:
    """When send_smtp_test_email raises EmailConfigurationError, endpoint returns 400 with result=config-error."""
    _login(client)

    with patch(_SMTP_SEND_PATH, side_effect=EmailConfigurationError("SMTP host is required")):
        response = client.post(_SMTP_TEST_URL, headers={"X-CSRF-Token": "any-token"})

    assert response.status_code == 400
    body = response.json()
    assert body["result"] == "config-error"
    assert "message" in body
    assert "SMTP host is required" in body["message"]


def test_smtp_test_delivery_error_returns_502(client: TestClient) -> None:
    """When send_smtp_test_email raises EmailDeliveryError, endpoint returns 502 with result=failed."""
    _login(client)

    with patch(_SMTP_SEND_PATH, side_effect=EmailDeliveryError("connection refused")):
        response = client.post(_SMTP_TEST_URL, headers={"X-CSRF-Token": "any-token"})

    assert response.status_code == 502
    body = response.json()
    assert body["result"] == "failed"
    assert "message" in body
    assert "connection refused" in body["message"]


def test_smtp_test_response_does_not_echo_smtp_password(client: TestClient) -> None:
    """The SMTP password stored in config must not appear in any API response body."""
    _login(client)

    smtp_password = "s3cr3t-smtp-pass"

    # Store a fake SMTP password in config
    payload = _full_config_payload({"SMTP_PASSWORD": smtp_password})
    client.put(
        "/api/config",
        json={"updates": payload},
        headers={"X-CSRF-Token": "token"},
    )

    # Simulate a delivery error whose message has already been sanitised by the
    # service layer (i.e. the password does not appear in the exception text).
    # This mirrors production behaviour: email.py's _sanitize_error_message
    # replaces any password occurrence with "[redacted]" before raising.
    with patch(
        _SMTP_SEND_PATH,
        side_effect=EmailDeliveryError("authentication failure: [redacted]"),
    ):
        response = client.post(_SMTP_TEST_URL, headers={"X-CSRF-Token": "token"})

    assert response.status_code == 502
    body = response.json()
    assert "result" in body
    assert "message" in body
    # The plaintext password must not appear anywhere in the response body
    assert smtp_password not in response.text


# ---------------------------------------------------------------------------
# M5-T08: MQTT / HA Discovery / Modbus CONFIG_FIELDS
# ---------------------------------------------------------------------------


def test_get_config_includes_mqtt_section(client: TestClient) -> None:
    """GET /api/config must include an MQTT section with expected fields."""
    _login(client)

    response = client.get("/api/config")
    body = response.json()

    section_names = {s["name"] for s in body["sections"]}
    assert "MQTT" in section_names, f"MQTT section missing; got {section_names}"

    mqtt_section = next(s for s in body["sections"] if s["name"] == "MQTT")
    env_names = {f["env_name"] for f in mqtt_section["fields"]}
    assert "MQTT_ENABLED" in env_names
    assert "MQTT_BROKER_HOST" in env_names
    assert "MQTT_BROKER_PORT" in env_names
    assert "MQTT_USERNAME" in env_names
    assert "MQTT_PASSWORD" in env_names
    assert "MQTT_TLS_ENABLED" in env_names


def test_get_config_includes_ha_discovery_section(client: TestClient) -> None:
    """GET /api/config must include a Home Assistant Discovery section."""
    _login(client)

    response = client.get("/api/config")
    body = response.json()

    section_names = {s["name"] for s in body["sections"]}
    assert "Home Assistant Discovery" in section_names, (
        f"Home Assistant Discovery section missing; got {section_names}"
    )

    ha_section = next(s for s in body["sections"] if s["name"] == "Home Assistant Discovery")
    env_names = {f["env_name"] for f in ha_section["fields"]}
    assert "HA_DISCOVERY_ENABLED" in env_names
    assert "HA_DISCOVERY_PREFIX" in env_names


def test_get_config_includes_modbus_section(client: TestClient) -> None:
    """GET /api/config must include a Modbus section with MODBUS_POLLING_ENABLED."""
    _login(client)

    response = client.get("/api/config")
    body = response.json()

    section_names = {s["name"] for s in body["sections"]}
    assert "Modbus" in section_names, f"Modbus section missing; got {section_names}"

    modbus_section = next(s for s in body["sections"] if s["name"] == "Modbus")
    env_names = {f["env_name"] for f in modbus_section["fields"]}
    assert "MODBUS_POLLING_ENABLED" in env_names


def test_get_config_mqtt_password_is_secret(client: TestClient) -> None:
    """MQTT_PASSWORD must be marked secret and return empty string in GET response."""
    _login(client)

    response = client.get("/api/config")
    body = response.json()

    mqtt_section = next(s for s in body["sections"] if s["name"] == "MQTT")
    password_field = next(f for f in mqtt_section["fields"] if f["env_name"] == "MQTT_PASSWORD")

    assert password_field["secret"] is True
    assert password_field["value"] == "", (
        f"MQTT_PASSWORD should be masked (empty string), got {password_field['value']!r}"
    )


def test_mqtt_broker_port_input_type_is_number(client: TestClient) -> None:
    """MQTT_BROKER_PORT must have input_type='number' for correct frontend rendering."""
    _login(client)

    response = client.get("/api/config")
    body = response.json()

    mqtt_section = next(s for s in body["sections"] if s["name"] == "MQTT")
    port_field = next(f for f in mqtt_section["fields"] if f["env_name"] == "MQTT_BROKER_PORT")

    assert port_field["input_type"] == "number", (
        f"MQTT_BROKER_PORT input_type should be 'number', got {port_field['input_type']!r}"
    )


def test_put_config_blank_mqtt_password_keeps_existing(
    client: TestClient, test_database_urls
) -> None:
    """Submitting blank MQTT_PASSWORD must NOT overwrite the stored secret."""
    _login(client)

    # Store a secret via full PUT
    payload_with_secret = _full_config_payload({"MQTT_PASSWORD": "mqtt-secret-value"})
    resp1 = client.put(
        "/api/config",
        json={"updates": payload_with_secret},
        headers={"X-CSRF-Token": "token"},
    )
    assert resp1.status_code == 200, f"First PUT failed: {resp1.status_code} {resp1.text}"

    # PUT with blank for that secret (keep-old semantics)
    payload_blank_secret = _full_config_payload({"MQTT_PASSWORD": ""})
    resp2 = client.put(
        "/api/config",
        json={"updates": payload_blank_secret},
        headers={"X-CSRF-Token": "token"},
    )
    assert resp2.status_code == 200, f"Second PUT failed: {resp2.status_code} {resp2.text}"

    # The stored value in the DB should still be the original secret
    conn = sqlite3.connect(test_database_urls["app_path"])
    try:
        rows = dict(conn.execute("SELECT key, value FROM app_config").fetchall())
    finally:
        conn.close()

    assert rows.get("MQTT_PASSWORD") == "mqtt-secret-value"


def test_put_config_invalid_mqtt_port_returns_422_and_does_not_write(
    client: TestClient, test_database_urls
) -> None:
    """Non-numeric MQTT_BROKER_PORT must return 422 and not persist the bad value."""
    _login(client)

    payload = _full_config_payload({"MQTT_BROKER_PORT": "not-a-number"})
    response = client.put(
        "/api/config",
        json={"updates": payload},
        headers={"X-CSRF-Token": "token"},
    )

    assert response.status_code == 422

    conn = sqlite3.connect(test_database_urls["app_path"])
    try:
        rows = dict(conn.execute("SELECT key, value FROM app_config").fetchall())
    finally:
        conn.close()

    assert rows.get("MQTT_BROKER_PORT") != "not-a-number"


# ---------------------------------------------------------------------------
# M5-polish2 Area B: bool fields have input_type="checkbox"
# ---------------------------------------------------------------------------

EXPECTED_CHECKBOX_FIELDS = {
    "APP_DEBUG",
    "SMTP_ENABLED",
    "SMTP_USE_STARTTLS",
    "AUTH_LOGIN_THROTTLE_ENABLED",
    "MQTT_ENABLED",
    "MQTT_TLS_ENABLED",
    "HA_DISCOVERY_ENABLED",
    "MODBUS_POLLING_ENABLED",
    "DSMR_INGEST_ENABLED",
}


def test_bool_fields_have_checkbox_input_type(client: TestClient) -> None:
    """All pure-bool config fields must return input_type='checkbox' from GET /api/config."""
    _login(client)

    response = client.get("/api/config")
    assert response.status_code == 200
    body = response.json()

    all_fields = {
        field["env_name"]: field
        for section in body["sections"]
        for field in section["fields"]
    }

    for env_name in EXPECTED_CHECKBOX_FIELDS:
        assert env_name in all_fields, f"{env_name} not found in config response"
        actual_type = all_fields[env_name]["input_type"]
        assert actual_type == "checkbox", (
            f"{env_name}: expected input_type='checkbox', got {actual_type!r}"
        )


def test_auth_cookie_secure_override_is_not_checkbox(client: TestClient) -> None:
    """AUTH_COOKIE_SECURE_OVERRIDE is bool|None (three-state) and must NOT be checkbox."""
    _login(client)

    response = client.get("/api/config")
    assert response.status_code == 200
    body = response.json()

    all_fields = {
        field["env_name"]: field
        for section in body["sections"]
        for field in section["fields"]
    }

    # This field is present in CONFIG_FIELDS; its type is bool|None so it stays text.
    if "AUTH_COOKIE_SECURE_OVERRIDE" in all_fields:
        actual_type = all_fields["AUTH_COOKIE_SECURE_OVERRIDE"]["input_type"]
        assert actual_type != "checkbox", (
            f"AUTH_COOKIE_SECURE_OVERRIDE should not be checkbox (it is bool|None); got {actual_type!r}"
        )


def test_put_config_bool_field_roundtrip_true_false(
    client: TestClient, test_database_urls
) -> None:
    """Saving 'true'/'false' string for a bool checkbox field roundtrips correctly."""
    _login(client)

    # Set MQTT_ENABLED to 'true' via config PUT
    payload_true = _full_config_payload({"MQTT_ENABLED": "true"})
    resp_true = client.put(
        "/api/config",
        json={"updates": payload_true},
        headers={"X-CSRF-Token": "token"},
    )
    assert resp_true.status_code == 200

    conn = sqlite3.connect(test_database_urls["app_path"])
    try:
        rows = dict(conn.execute("SELECT key, value FROM app_config").fetchall())
    finally:
        conn.close()
    assert rows.get("MQTT_ENABLED") == "true"

    # Now set to 'false'
    payload_false = _full_config_payload({"MQTT_ENABLED": "false"})
    resp_false = client.put(
        "/api/config",
        json={"updates": payload_false},
        headers={"X-CSRF-Token": "token"},
    )
    assert resp_false.status_code == 200

    conn = sqlite3.connect(test_database_urls["app_path"])
    try:
        rows = dict(conn.execute("SELECT key, value FROM app_config").fetchall())
    finally:
        conn.close()
    assert rows.get("MQTT_ENABLED") == "false"


# ---------------------------------------------------------------------------
# M5-fix2: DB-config → runtime settings (Issue 2 regression assertions)
# ---------------------------------------------------------------------------

def test_put_config_mqtt_reconnect_uses_db_merged_settings(
    client: TestClient, test_database_urls
) -> None:
    """PUT /api/config with MQTT keys must reconnect with DB-merged settings,
    not bootstrap-only settings.  Asserts that mqtt_manager.reconnect receives
    settings where mqtt_enabled=True and mqtt_broker_host matches the submitted value.
    """
    _login(client)

    target_host = "mqtt.example.com"
    payload = _full_config_payload({
        "MQTT_ENABLED": "true",
        "MQTT_BROKER_HOST": target_host,
    })

    mock_mgr = MagicMock()
    mock_mgr.is_configured.return_value = True

    with patch("app.api.routes.api.config.mqtt_manager", mock_mgr):
        resp = client.put(
            "/api/config",
            json={"updates": payload},
            headers={"X-CSRF-Token": "token"},
        )

    assert resp.status_code == 200

    # reconnect must have been called once
    assert mock_mgr.reconnect.call_count == 1, (
        f"Expected mqtt_manager.reconnect called once, got {mock_mgr.reconnect.call_count}"
    )
    reconnect_settings = mock_mgr.reconnect.call_args[0][0]
    assert reconnect_settings.mqtt_enabled is True, (
        f"reconnect settings.mqtt_enabled must be True, got {reconnect_settings.mqtt_enabled!r}"
    )
    assert reconnect_settings.mqtt_broker_host == target_host, (
        f"reconnect settings.mqtt_broker_host must be {target_host!r}, "
        f"got {reconnect_settings.mqtt_broker_host!r}"
    )


# ---------------------------------------------------------------------------
# M6-T02: DSMR + Tibber CONFIG_FIELDS
# ---------------------------------------------------------------------------


def test_get_config_includes_dsmr_section(client: TestClient) -> None:
    """GET /api/config must include a DSMR section with expected fields including DSMR_TARIFF_TOPIC."""
    _login(client)

    response = client.get("/api/config")
    body = response.json()

    section_names = {s["name"] for s in body["sections"]}
    assert "DSMR" in section_names, f"DSMR section missing; got {section_names}"

    dsmr_section = next(s for s in body["sections"] if s["name"] == "DSMR")
    env_names = {f["env_name"] for f in dsmr_section["fields"]}
    assert "DSMR_INGEST_ENABLED" in env_names
    assert "DSMR_MQTT_TOPIC" in env_names
    assert "DSMR_SAMPLE_INTERVAL_S" in env_names
    assert "DSMR_TARIFF_TOPIC" in env_names, (
        f"DSMR_TARIFF_TOPIC must be present in DSMR section; got {env_names}"
    )


def test_get_config_includes_tibber_section(client: TestClient) -> None:
    """GET /api/config must include a Tibber section with expected fields."""
    _login(client)

    response = client.get("/api/config")
    body = response.json()

    section_names = {s["name"] for s in body["sections"]}
    assert "Tibber" in section_names, f"Tibber section missing; got {section_names}"

    tibber_section = next(s for s in body["sections"] if s["name"] == "Tibber")
    env_names = {f["env_name"] for f in tibber_section["fields"]}
    assert "TIBBER_API_TOKEN" in env_names
    assert "TIBBER_HOME_ID" in env_names


def test_get_config_tibber_api_token_is_secret(client: TestClient) -> None:
    """TIBBER_API_TOKEN must be marked secret and return empty string in GET response."""
    _login(client)

    response = client.get("/api/config")
    body = response.json()

    tibber_section = next(s for s in body["sections"] if s["name"] == "Tibber")
    token_field = next(f for f in tibber_section["fields"] if f["env_name"] == "TIBBER_API_TOKEN")

    assert token_field["secret"] is True
    assert token_field["value"] == "", (
        f"TIBBER_API_TOKEN should be masked (empty string), got {token_field['value']!r}"
    )


def test_get_config_dsmr_ingest_enabled_is_checkbox(client: TestClient) -> None:
    """DSMR_INGEST_ENABLED must have input_type='checkbox' for correct frontend rendering."""
    _login(client)

    response = client.get("/api/config")
    body = response.json()

    dsmr_section = next(s for s in body["sections"] if s["name"] == "DSMR")
    enabled_field = next(f for f in dsmr_section["fields"] if f["env_name"] == "DSMR_INGEST_ENABLED")

    assert enabled_field["input_type"] == "checkbox", (
        f"DSMR_INGEST_ENABLED input_type should be 'checkbox', got {enabled_field['input_type']!r}"
    )


def test_get_config_dsmr_sample_interval_input_type_is_number(client: TestClient) -> None:
    """DSMR_SAMPLE_INTERVAL_S must have input_type='number'."""
    _login(client)

    response = client.get("/api/config")
    body = response.json()

    dsmr_section = next(s for s in body["sections"] if s["name"] == "DSMR")
    interval_field = next(
        f for f in dsmr_section["fields"] if f["env_name"] == "DSMR_SAMPLE_INTERVAL_S"
    )

    assert interval_field["input_type"] == "number", (
        f"DSMR_SAMPLE_INTERVAL_S input_type should be 'number', got {interval_field['input_type']!r}"
    )


def test_put_config_blank_tibber_api_token_keeps_existing(
    client: TestClient, test_database_urls
) -> None:
    """Submitting blank TIBBER_API_TOKEN must NOT overwrite the stored secret."""
    _login(client)

    # Store a secret via full PUT
    payload_with_secret = _full_config_payload({"TIBBER_API_TOKEN": "tibber-secret-token"})
    resp1 = client.put(
        "/api/config",
        json={"updates": payload_with_secret},
        headers={"X-CSRF-Token": "token"},
    )
    assert resp1.status_code == 200, f"First PUT failed: {resp1.status_code} {resp1.text}"

    # PUT with blank for that secret (keep-old semantics)
    payload_blank_secret = _full_config_payload({"TIBBER_API_TOKEN": ""})
    resp2 = client.put(
        "/api/config",
        json={"updates": payload_blank_secret},
        headers={"X-CSRF-Token": "token"},
    )
    assert resp2.status_code == 200, f"Second PUT failed: {resp2.status_code} {resp2.text}"

    # The stored value in the DB should still be the original secret
    conn = sqlite3.connect(test_database_urls["app_path"])
    try:
        rows = dict(conn.execute("SELECT key, value FROM app_config").fetchall())
    finally:
        conn.close()

    assert rows.get("TIBBER_API_TOKEN") == "tibber-secret-token"


def test_put_config_new_tibber_api_token_overwrites_existing(
    client: TestClient, test_database_urls
) -> None:
    """Submitting a non-blank TIBBER_API_TOKEN must overwrite the stored secret."""
    _login(client)

    # Store initial secret
    payload_initial = _full_config_payload({"TIBBER_API_TOKEN": "old-tibber-token"})
    resp1 = client.put(
        "/api/config",
        json={"updates": payload_initial},
        headers={"X-CSRF-Token": "token"},
    )
    assert resp1.status_code == 200

    # Overwrite with a new non-blank secret
    payload_new = _full_config_payload({"TIBBER_API_TOKEN": "new-tibber-token"})
    resp2 = client.put(
        "/api/config",
        json={"updates": payload_new},
        headers={"X-CSRF-Token": "token"},
    )
    assert resp2.status_code == 200

    conn = sqlite3.connect(test_database_urls["app_path"])
    try:
        rows = dict(conn.execute("SELECT key, value FROM app_config").fetchall())
    finally:
        conn.close()

    assert rows.get("TIBBER_API_TOKEN") == "new-tibber-token"


def test_put_config_invalid_dsmr_sample_interval_returns_422_and_does_not_write(
    client: TestClient, test_database_urls
) -> None:
    """Non-integer DSMR_SAMPLE_INTERVAL_S must return 422 and not persist the bad value."""
    _login(client)

    payload = _full_config_payload({"DSMR_SAMPLE_INTERVAL_S": "not-a-number"})
    response = client.put(
        "/api/config",
        json={"updates": payload},
        headers={"X-CSRF-Token": "token"},
    )

    assert response.status_code == 422

    conn = sqlite3.connect(test_database_urls["app_path"])
    try:
        rows = dict(conn.execute("SELECT key, value FROM app_config").fetchall())
    finally:
        conn.close()

    assert rows.get("DSMR_SAMPLE_INTERVAL_S") != "not-a-number"


def test_put_config_dsmr_tariff_topic_persists_and_reflects_in_get(
    client: TestClient, test_database_urls
) -> None:
    """DSMR_TARIFF_TOPIC must persist via PUT and be readable via GET /api/config."""
    _login(client)

    new_topic = "meter/tariff/slot"
    payload = _full_config_payload({"DSMR_TARIFF_TOPIC": new_topic})
    with patch("app.services.dsmr_ingest.apply_dsmr_subscription"):
        response = client.put(
            "/api/config",
            json={"updates": payload},
            headers={"X-CSRF-Token": "token"},
        )
    assert response.status_code == 200

    # The updated value must appear in the GET response.
    get_resp = client.get("/api/config")
    body = get_resp.json()
    dsmr_section = next(s for s in body["sections"] if s["name"] == "DSMR")
    tariff_field = next(f for f in dsmr_section["fields"] if f["env_name"] == "DSMR_TARIFF_TOPIC")
    assert tariff_field["value"] == new_topic, (
        f"Expected DSMR_TARIFF_TOPIC to be {new_topic!r}, got {tariff_field['value']!r}"
    )


def test_put_config_dsmr_tariff_topic_in_settings_payload(client: TestClient) -> None:
    """DSMR_TARIFF_TOPIC must appear in _settings_payload (GET /api/config returns it)."""
    _login(client)

    # The default value from Settings must appear in the DSMR section.
    response = client.get("/api/config")
    body = response.json()

    dsmr_section = next(s for s in body["sections"] if s["name"] == "DSMR")
    tariff_field = next(
        (f for f in dsmr_section["fields"] if f["env_name"] == "DSMR_TARIFF_TOPIC"), None
    )
    assert tariff_field is not None, "DSMR_TARIFF_TOPIC must appear in DSMR config section"
    # Default value should be the DSMR reader meter-stats topic.
    assert tariff_field["value"] == "dsmr/meter-stats/electricity_tariff", (
        f"Unexpected default for DSMR_TARIFF_TOPIC: {tariff_field['value']!r}"
    )


def test_get_config_tibber_api_token_value_masked_after_save(
    client: TestClient, test_database_urls
) -> None:
    """After saving a TIBBER_API_TOKEN, the value must remain masked (empty string) in GET response."""
    _login(client)

    # Save a non-blank token
    payload = _full_config_payload({"TIBBER_API_TOKEN": "some-tibber-token"})
    put_resp = client.put(
        "/api/config",
        json={"updates": payload},
        headers={"X-CSRF-Token": "token"},
    )
    assert put_resp.status_code == 200

    # After saving: value must still be masked (empty string), configured must be True
    resp_after = client.get("/api/config")
    body_after = resp_after.json()
    tibber_section_after = next(s for s in body_after["sections"] if s["name"] == "Tibber")
    token_field_after = next(
        f for f in tibber_section_after["fields"] if f["env_name"] == "TIBBER_API_TOKEN"
    )
    assert token_field_after["configured"] is True
    assert token_field_after["value"] == "", "Secret value must remain masked after save"
    # The actual token must not appear anywhere in the response body
    assert "some-tibber-token" not in resp_after.text


def test_post_mqtt_test_uses_db_broker_host(
    client: TestClient, test_database_urls
) -> None:
    """POST /api/config/mqtt/test must use the DB-stored broker host, not env/bootstrap."""
    _login(client)

    db_host = "broker.db-configured.local"

    # First write the MQTT config to DB via PUT /api/config
    payload_save = _full_config_payload({
        "MQTT_ENABLED": "true",
        "MQTT_BROKER_HOST": db_host,
    })
    with patch("app.api.routes.api.config.mqtt_manager"):
        save_resp = client.put(
            "/api/config",
            json={"updates": payload_save},
            headers={"X-CSRF-Token": "token"},
        )
    assert save_resp.status_code == 200

    # Now POST /api/config/mqtt/test — capture the host that _run_mqtt_test uses.
    captured_host: list[str] = []

    def _fake_run_mqtt_test(settings):
        captured_host.append(settings.mqtt_broker_host)
        # simulate success by just returning
        return

    with patch("app.api.routes.api.config._run_mqtt_test", side_effect=_fake_run_mqtt_test):
        resp = client.post(
            "/api/config/mqtt/test",
            headers={"X-CSRF-Token": "token"},
        )

    assert resp.status_code == 200
    assert len(captured_host) == 1, "Expected _run_mqtt_test to be called once"
    assert captured_host[0] == db_host, (
        f"Expected mqtt test to use DB host {db_host!r}, got {captured_host[0]!r}"
    )
