"""Tests for the gsl3670 touchscreen configuration validation."""

from __future__ import annotations

from pathlib import Path

import pytest

from esphome import config_validation as cv
from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32
from esphome.components.gsl3670 import touchscreen as gsl
from esphome.const import (
    CONF_CALIBRATION,
    CONF_INTERRUPT_PIN,
    CONF_MODEL,
    CONF_RESET_PIN,
    CONF_TRANSFORM,
    PlatformFramework,
)
from tests.component_tests.types import SetCoreConfigCallable

VALID_URL = "https://example.com/fw.bin"


def _make_firmware(blocks: int = 2) -> bytes:
    """Build a structurally valid firmware blob with ``blocks`` blocks.

    Each block is ``_FW_BLK_SIZE`` bytes: a 4-byte header (page address <= 0xEF
    followed by the 1/2/3 marker bytes) and a 128-byte payload.
    """
    out = bytearray()
    for i in range(blocks):
        out += bytes([i, 1, 2, 3]) + bytes(gsl._FW_BLK_SIZE - 4)
    return bytes(out)


def _write_firmware(tmp_path: Path, data: bytes | None = None) -> Path:
    """Write firmware bytes to a temp file and return its path."""
    path = tmp_path / "fw.bin"
    path.write_bytes(_make_firmware() if data is None else data)
    return path


# ---------------------------------------------------------------------------
# _validate_firmware_data - blob structure
# ---------------------------------------------------------------------------


def test_validate_firmware_data_accepts_valid_blob() -> None:
    """A correctly structured blob passes validation."""
    gsl._validate_firmware_data(_make_firmware(3), "test")


@pytest.mark.parametrize("length", [0, gsl._FW_BLK_SIZE - 1, gsl._FW_BLK_SIZE + 1])
def test_validate_firmware_data_rejects_bad_length(length: int) -> None:
    """The blob length must be a non-zero multiple of the block size."""
    with pytest.raises(cv.Invalid, match="length is incorrect"):
        gsl._validate_firmware_data(bytes(length), "test")


@pytest.mark.parametrize(
    "index,value",
    [
        (0, 0xF0),  # page address must be <= 0xEF
        (1, 0x00),  # marker byte must be 1
        (2, 0x00),  # marker byte must be 2
        (3, 0x00),  # marker byte must be 3
    ],
)
def test_validate_firmware_data_rejects_corrupted_header(
    index: int, value: int
) -> None:
    """A block whose header bytes are wrong is reported as corrupted."""
    data = bytearray(_make_firmware(2))
    # Corrupt the header of the second block.
    data[gsl._FW_BLK_SIZE + index] = value
    with pytest.raises(cv.Invalid, match="Corrupted firmware at block 1"):
        gsl._validate_firmware_data(bytes(data), "test")


# ---------------------------------------------------------------------------
# _cache_path / firmware_path
# ---------------------------------------------------------------------------


def test_cache_path_is_deterministic_per_url(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    """The cache path is derived from (and stable for) the URL."""
    monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path))
    first = gsl._cache_path(VALID_URL)
    assert first == gsl._cache_path(VALID_URL)
    assert first != gsl._cache_path("https://example.com/other.bin")
    assert first.parent == tmp_path / "gsl3670"


def test_firmware_path_prefers_local_file(tmp_path: Path) -> None:
    """A ``file`` source is returned as-is, without consulting the cache."""
    path = _write_firmware(tmp_path)
    assert gsl.firmware_path({"file": path}) == path


def test_firmware_path_uses_cache_for_url(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    """A ``url`` source resolves to the cache path for that URL."""
    monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path))
    assert gsl.firmware_path({"url": VALID_URL}) == gsl._cache_path(VALID_URL)


# ---------------------------------------------------------------------------
# _validate_firmware / FIRMWARE_SCHEMA
# ---------------------------------------------------------------------------


def test_firmware_requires_exactly_one_source(tmp_path: Path) -> None:
    """Supplying both, or neither, of url/file is an error."""
    path = _write_firmware(tmp_path)
    with pytest.raises(cv.Invalid, match="Exactly one"):
        gsl._validate_firmware({"url": VALID_URL, "file": path})
    with pytest.raises(cv.Invalid, match="Exactly one"):
        gsl._validate_firmware({})


def test_firmware_file_valid(tmp_path: Path) -> None:
    """A valid firmware file passes the full FIRMWARE_SCHEMA."""
    path = _write_firmware(tmp_path)
    result = gsl.FIRMWARE_SCHEMA({"file": str(path)})
    assert result["file"] == path


def test_firmware_file_corrupt_rejected(tmp_path: Path) -> None:
    """A file whose contents fail the structural check is rejected."""
    path = _write_firmware(tmp_path, data=b"\x00" * (gsl._FW_BLK_SIZE * 2))
    with pytest.raises(cv.Invalid, match="Corrupted firmware"):
        gsl._validate_firmware({"file": path})


def test_firmware_url_downloads_and_validates(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    """A url source downloads the content and validates its structure."""
    data = _make_firmware()
    monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path))
    monkeypatch.setattr(gsl.external_files, "download_content", lambda url, path: data)
    assert gsl._validate_firmware({"url": VALID_URL}) == {"url": VALID_URL}


def test_firmware_url_sha256_mismatch_rejected(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    """A configured SHA-256 that does not match the download is rejected."""
    data = _make_firmware()
    monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path))
    monkeypatch.setattr(gsl.external_files, "download_content", lambda url, path: data)
    with pytest.raises(cv.Invalid, match="SHA-256 mismatch"):
        gsl._validate_firmware({"url": VALID_URL, "sha256": "00" * 32})


def test_firmware_url_invalid_structure_rejected(
    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
    """Downloaded content that is not a valid blob is rejected."""
    monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path))
    monkeypatch.setattr(
        gsl.external_files, "download_content", lambda url, path: b"\x00\x01\x02"
    )
    with pytest.raises(cv.Invalid, match="length is incorrect"):
        gsl._validate_firmware({"url": VALID_URL})


# ---------------------------------------------------------------------------
# CONFIG_SCHEMA
# ---------------------------------------------------------------------------


@pytest.fixture(autouse=True)
def _esp32_core(set_core_config: SetCoreConfigCallable) -> None:
    """Configure the core as an ESP32 target for the schema tests."""
    set_core_config(
        PlatformFramework.ESP32_IDF,
        platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
    )


def test_config_custom_model_minimal(tmp_path: Path) -> None:
    """The CUSTOM model validates with an explicit firmware file and pins."""
    fw = _write_firmware(tmp_path)
    result = gsl.CONFIG_SCHEMA(
        {
            "model": "custom",
            "interrupt_pin": 16,
            "reset_pin": 4,
            "firmware": {"file": str(fw)},
        }
    )
    assert result[CONF_MODEL] == "CUSTOM"
    assert "id" in result
    # The CUSTOM model supplies no transform/calibration defaults.
    assert CONF_TRANSFORM not in result
    assert CONF_CALIBRATION not in result


def test_config_custom_model_requires_firmware() -> None:
    """The firmware option is required for the CUSTOM model (no default)."""
    with pytest.raises(cv.Invalid, match=r"required key not provided.*firmware"):
        gsl.CONFIG_SCHEMA({"model": "custom", "interrupt_pin": 16, "reset_pin": 4})


def test_config_invalid_model_rejected() -> None:
    """An unknown model name is rejected."""
    with pytest.raises(cv.Invalid, match="model"):
        gsl.CONFIG_SCHEMA({"model": "nonexistent"})


def test_config_seeed_model_applies_defaults(tmp_path: Path) -> None:
    """The SEEED model populates transform and calibration defaults.

    ``reset_pin`` is overridden with a plain GPIO so the test does not depend on
    the model's default IO-expander pin.
    """
    fw = _write_firmware(tmp_path)
    result = gsl.CONFIG_SCHEMA(
        {
            "model": "seeed-reterminal-d1001",
            "reset_pin": 4,
            "firmware": {"file": str(fw)},
        }
    )
    assert result[CONF_MODEL] == "SEEED-RETERMINAL-D1001"
    # Transform defaults from the model.
    assert result[CONF_TRANSFORM] == {
        "swap_xy": True,
        "mirror_x": True,
        "mirror_y": True,
    }
    # Calibration defaults from the model.
    assert result[CONF_CALIBRATION]["x_min"] == 20
    assert result[CONF_CALIBRATION]["x_max"] == 872
    assert result[CONF_CALIBRATION]["y_min"] == 20
    assert result[CONF_CALIBRATION]["y_max"] == 1644
    # The interrupt pin default (16) is applied without being specified.
    assert CONF_INTERRUPT_PIN in result
    assert CONF_RESET_PIN in result


def test_config_guition_model_applies_defaults(tmp_path: Path) -> None:
    """The GUITION model populates transform and calibration defaults."""
    fw = _write_firmware(tmp_path)
    result = gsl.CONFIG_SCHEMA(
        {
            "model": "guition-jc8012p4a1",
            "firmware": {"file": str(fw)},
        }
    )
    assert result[CONF_MODEL] == "GUITION-JC8012P4A1"
    # Transform defaults from the model.
    assert result[CONF_TRANSFORM] == {
        "swap_xy": True,
        "mirror_x": True,
        "mirror_y": False,
    }
    # Calibration defaults from the model.
    assert result[CONF_CALIBRATION]["x_min"] == 20
    assert result[CONF_CALIBRATION]["x_max"] == 880
    assert result[CONF_CALIBRATION]["y_min"] == 20
    assert result[CONF_CALIBRATION]["y_max"] == 1648
    assert result[CONF_INTERRUPT_PIN]["number"] == 21
    assert result[CONF_RESET_PIN]["number"] == 22


def test_config_rejects_non_dict() -> None:
    """A non-dict configuration is rejected."""
    with pytest.raises(cv.Invalid, match="expected a dictionary"):
        gsl.CONFIG_SCHEMA("not a dict")
