import glob
import logging
import os

from esphome import yaml_util
from esphome.components import substitutions
from esphome.const import CONF_PACKAGES

_LOGGER = logging.getLogger(__name__)

# Set to True for dev mode behavior
# This will generate the expected version of the test files.

DEV_MODE = False


def sort_dicts(obj):
    """Recursively sort dictionaries for order-insensitive comparison."""
    if isinstance(obj, dict):
        return {k: sort_dicts(obj[k]) for k in sorted(obj)}
    elif isinstance(obj, list):
        # Lists are not sorted; we preserve order
        return [sort_dicts(i) for i in obj]
    else:
        return obj


def dict_diff(a, b, path=""):
    """Recursively find differences between two dict/list structures."""
    diffs = []
    if isinstance(a, dict) and isinstance(b, dict):
        a_keys = set(a)
        b_keys = set(b)
        for key in a_keys - b_keys:
            diffs.append(f"{path}/{key} only in actual")
        for key in b_keys - a_keys:
            diffs.append(f"{path}/{key} only in expected")
        for key in a_keys & b_keys:
            diffs.extend(dict_diff(a[key], b[key], f"{path}/{key}"))
    elif isinstance(a, list) and isinstance(b, list):
        min_len = min(len(a), len(b))
        for i in range(min_len):
            diffs.extend(dict_diff(a[i], b[i], f"{path}[{i}]"))
        if len(a) > len(b):
            for i in range(min_len, len(a)):
                diffs.append(f"{path}[{i}] only in actual: {a[i]!r}")
        elif len(b) > len(a):
            for i in range(min_len, len(b)):
                diffs.append(f"{path}[{i}] only in expected: {b[i]!r}")
    else:
        if a != b:
            diffs.append(f"\t{path}: actual={a!r} expected={b!r}")
    return diffs


def write_yaml(path, data):
    with open(path, "w", encoding="utf-8") as f:
        f.write(yaml_util.dump(data))


def test_substitutions_fixtures(fixture_path):
    base_dir = fixture_path / "substitutions"
    sources = sorted(glob.glob(str(base_dir / "*.input.yaml")))
    assert sources, f"No input YAML files found in {base_dir}"

    failures = []
    for source_path in sources:
        try:
            expected_path = source_path.replace(".input.yaml", ".approved.yaml")
            test_case = os.path.splitext(os.path.basename(source_path))[0].replace(
                ".input", ""
            )

            # Load using ESPHome's YAML loader
            config = yaml_util.load_yaml(source_path)

            if CONF_PACKAGES in config:
                from esphome.components.packages import do_packages_pass

                config = do_packages_pass(config)

            substitutions.do_substitution_pass(config, None)

            # Also load expected using ESPHome's loader, or use {} if missing and DEV_MODE
            if os.path.isfile(expected_path):
                expected = yaml_util.load_yaml(expected_path)
            elif DEV_MODE:
                expected = {}
            else:
                assert os.path.isfile(expected_path), (
                    f"Expected file missing: {expected_path}"
                )

            # Sort dicts only (not lists) for comparison
            got_sorted = sort_dicts(config)
            expected_sorted = sort_dicts(expected)

            if got_sorted != expected_sorted:
                diff = "\n".join(dict_diff(got_sorted, expected_sorted))
                msg = (
                    f"Substitution result mismatch for {os.path.basename(source_path)}\n"
                    f"Diff:\n{diff}\n\n"
                    f"Got:      {got_sorted}\n"
                    f"Expected: {expected_sorted}"
                )
                # Write out the received file when test fails
                if DEV_MODE:
                    received_path = os.path.join(
                        os.path.dirname(source_path), f"{test_case}.received.yaml"
                    )
                    write_yaml(received_path, config)
                    print(msg)
                    failures.append(msg)
                else:
                    raise AssertionError(msg)
        except Exception as err:
            _LOGGER.error("Error in test file %s", source_path)
            raise err

    if DEV_MODE and failures:
        print(f"\n{len(failures)} substitution test case(s) failed.")

    if DEV_MODE:
        _LOGGER.error("Tests passed, but Dev mode is enabled.")
    assert not DEV_MODE  # make sure DEV_MODE is disabled after you are finished.
