# tests/test_imports_and_schedules.py
import json
from pathlib import Path

import pytest

from hassl.cli import parse_hassl  # parser + transformer
from hassl.semantics import analyzer as sem_analyzer  # to set GLOBAL_EXPORTS
from hassl.semantics.analyzer import analyze         # analyzer -> IR
from hassl.ast.nodes import Alias, Schedule          # node types for exports
from hassl.codegen import rules_min                  # codegen entry (rules)
from hassl.codegen import package as pkg_codegen     # package-level codegen (schedules/helpers)


# --------- helpers (mirrors cli.py logic lightly) ---------

def _collect_public_exports(prog, pkg: str):
    out = {}
    for s in prog.statements:
        if isinstance(s, Alias) and not getattr(s, "private", False):
            out[(pkg, "alias", s.name)] = s
    for s in prog.statements:
        if isinstance(s, Schedule) and not getattr(s, "private", False):
            out[(pkg, "schedule", s.name)] = s
        elif isinstance(s, dict) and s.get("type") == "schedule_decl" and not s.get("private", False):
            out[(pkg, "schedule", s["name"])] = Schedule(
                name=s["name"], clauses=s.get("clauses", []) or [], private=False
            )
    return out


def _write(p: Path, text: str):
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(text)


# =========== tests ===========

def test_import_glob_alias_and_schedule(tmp_path: Path):
    """home imports std.*; landing_light + wake_hours usable; schedule name normalized to pkg.name."""
    shared = tmp_path / "std" / "shared.hassl"
    landing = tmp_path / "home" / "landing.hassl"

    _write(shared, """
package std.shared

alias landing_light = light.landing_main
private alias _debug = light.dev_fixture

schedule wake_hours:
  enable from 07:00 to 23:00;
""")

    _write(landing, """
package home.landing
import std.shared.*

rule motion_light:
  schedule use wake_hours;
  if (motion && lux < 50)
  then landing_light = on; wait (!motion for 10m) landing_light = off
""")

    # parse both
    p_shared = parse_hassl(shared.read_text()); p_shared.package = "std.shared"
    p_landing = parse_hassl(landing.read_text()); p_landing.package = "home.landing"

    # build exports (like cli pass 1)
    sem_analyzer.GLOBAL_EXPORTS = _collect_public_exports(p_shared, "std.shared")

    # analyze home
    ir = analyze(p_landing).to_dict()

    # schedule use is present (base-name form as emitted by analyzer)
    (rule,) = ir["rules"]
    assert rule["schedule_uses"] == ["wake_hours"]

    # actions reference the alias name
    assigns = [a for a in rule["clauses"][0]["actions"] if a["type"] == "assign"]
    assert {a["target"] for a in assigns} == {"landing_light"}


def test_private_alias_not_imported(tmp_path: Path):
    """private alias is not visible to importer."""
    shared = tmp_path / "std" / "shared.hassl"
    landing = tmp_path / "home" / "landing.hassl"

    _write(shared, """
package std.shared
private alias hidden = light.foo
alias visible = light.bar
""")

    _write(landing, """
package home.landing
import std.shared.*

rule r:
  if (visible) then visible = on
""")

    p_shared = parse_hassl(shared.read_text()); p_shared.package = "std.shared"
    p_landing = parse_hassl(landing.read_text()); p_landing.package = "home.landing"

    sem_analyzer.GLOBAL_EXPORTS = _collect_public_exports(p_shared, "std.shared")

    ir = analyze(p_landing).to_dict()

    # verify that 'visible' (public) is referenced in actions and 'hidden' (private) never appears
    (rule,) = ir["rules"]
    assigns = [a for a in rule["clauses"][0]["actions"] if a["type"] == "assign"]
    targets = {a["target"] for a in assigns}
    assert "visible" in targets
    assert all("hidden" not in t for t in targets)


def test_declared_schedule_emits_sensor_and_rule_gates_on_it(tmp_path: Path):
    """
    Top-level schedule creates a template binary_sensor in package.py,
    and the rule generated by rules_min gates on that sensor.
    """
    shared = tmp_path / "std" / "shared.hassl"
    _write(shared, """
package std.shared
alias lamp = light.fake_lamp    
schedule wake_hours:
  enable from 07:00 to 23:00;

rule ping:
    schedule use wake_hours;
    if (lamp) then lamp = on
""")
    p_shared = parse_hassl(shared.read_text()); p_shared.package = "std.shared"
    sem_analyzer.GLOBAL_EXPORTS = {}  # standalone compile is fine

    ir_shared = analyze(p_shared)  # contains schedule + a rule that uses it

    outdir = tmp_path / "out_std"
    # Emit package (creates schedule sensor if IR exposes schedules)
    pkg_codegen.emit_package(ir_shared, str(outdir))
    # Emit rules (creates the rule automation that references the sensor)
    rules_min.generate_rules(ir_shared.to_dict(), str(outdir))

    pkg_slug = rules_min._pkg_slug(str(outdir))

    # Bundled automations should include a condition referencing that binary_sensor
    bundled = (outdir / f"rules_bundled_{pkg_slug}.yaml").read_text()
    assert f"binary_sensor.hassl_schedule_{pkg_slug}_wake_hours_active" in bundled

    # The schedules file may or may not be emitted depending on analyzer exposing schedules.
    # If present, it should contain the binary_sensor.
    sched_path = outdir / f"schedules_{pkg_slug}.yaml"
    if sched_path.exists():
        schedules_yaml = sched_path.read_text()
        assert "template:" in schedules_yaml
        assert "binary_sensor:" in schedules_yaml
        assert f"hassl_schedule_{pkg_slug}_wake_hours_active" in schedules_yaml


def test_inline_schedule_compiles_inline_conditions(tmp_path: Path):
    """
    Inline schedule generates direct HA conditions (no helpers), and the rule includes them
    when the analyzer surfaces inline schedule clauses.
    """
    landing = tmp_path / "home" / "landing.hassl"
    _write(landing, """
package home.landing

alias kitchen = light.kitchen
    
rule motion_light:
  schedule
    enable from sunrise+15m until 23:00;
  if (motion) then kitchen = on
""")
    p = parse_hassl(landing.read_text()); p.package = "home.landing"
    sem_analyzer.GLOBAL_EXPORTS = {}
    ir = analyze(p)

    outdir = tmp_path / "out_inline"
    # Package emit is optional here (no named schedules); still harmless
    pkg_codegen.emit_package(ir, str(outdir))
    rules_min.generate_rules(ir.to_dict(), str(outdir))

    pkg_slug = rules_min._pkg_slug(str(outdir))
    bundled = (outdir / f"rules_bundled_{pkg_slug}.yaml").read_text()

    # The rule automation should NOT reference any schedule binary_sensor (since inline)
    assert "binary_sensor.hassl_schedule_" not in bundled

def test_cross_package_import_and_codegen_gate(tmp_path: Path):
    """home uses std.wake_hours; codegen must gate the rule with the package schedule sensor."""
    shared = tmp_path / "std" / "shared.hassl"
    homep = tmp_path / "home" / "landing.hassl"

    _write(shared, """
package std.shared
alias landing_light = light.landing_main
schedule wake_hours:
  enable from 07:00 to 23:00;
""")
    _write(homep, """
package home.landing
import std.shared.*

rule motion_light:
  schedule use wake_hours;
  if (motion) then landing_light = on
""")

    # parse + export
    p_shared = parse_hassl(shared.read_text()); p_shared.package = "std.shared"
    sem_analyzer.GLOBAL_EXPORTS = _collect_public_exports(p_shared, "std.shared")

    # analyze home
    p_home = parse_hassl(homep.read_text()); p_home.package = "home.landing"
    ir_home = analyze(p_home)

    # generate combined YAML for the home package
    outdir = tmp_path / "out_home"
    pkg_codegen.emit_package(ir_home, str(outdir))  # no named schedules here; safe no-op
    rules_min.generate_rules(ir_home.to_dict(), str(outdir))

    pkg_slug = rules_min._pkg_slug(str(outdir))
    bundled = (outdir / f"rules_bundled_{pkg_slug}.yaml").read_text()

    # the rule’s condition gates on the schedule binary_sensor for the DECLARING pkg (std.shared → std_shared)
    assert "binary_sensor.hassl_schedule_std_shared_wake_hours_active" in bundled


def test_schedule_use_missing_declaration_raises(tmp_path: Path):
    """Using a schedule that isn't declared/imported in this package should raise a helpful error."""
    landing = tmp_path / "home" / "landing.hassl"
    _write(landing, """
package home.landing

alias kitchen = light.kitchen

rule motion_light:
  schedule use wake_hours;
  if (motion) then kitchen = on
""")
    p = parse_hassl(landing.read_text()); p.package = "home.landing"
    sem_analyzer.GLOBAL_EXPORTS = {}  # nothing imported
    ir = analyze(p)

    outdir = tmp_path / "out_err"
    pkg_slug = rules_min._pkg_slug(str(outdir))

    with pytest.raises(ValueError) as ei:
        rules_min.generate_rules(ir.to_dict(), str(outdir))

    msg = str(ei.value)
    assert "schedule reference not found" in msg
    assert "wake_hours" in msg
    # Should include the sensor id we would look for in this package
    assert f"binary_sensor.hassl_schedule_{pkg_slug}_wake_hours_active" in msg
