"""Tests for Condensa Code contract validation library."""

import pytest
from cdn.contracts import (
    parse_contract,
    parse_line,
    check,
    validate_wires,
    validate_all,
    FnContract,
    TypeContract,
    WireContract,
    DefContract,
    ApiContract,
    SchemaContract,
    ContractSet,
    CheckResult,
)
from cdn.contracts.types import PropSpec, FieldSpec


# ── PropSpec parsing ─────────────────────────────────────────────────

class TestPropSpec:
    def test_simple(self):
        p = PropSpec.parse("name:s")
        assert p.name == "name"
        assert p.type == "s"
        assert p.required is True

    def test_required_star(self):
        p = PropSpec.parse("name:s*")
        assert p.required is True

    def test_optional(self):
        p = PropSpec.parse("name:s?")
        assert p.required is False

    def test_array_type(self):
        p = PropSpec.parse("programs:Program[]")
        assert p.type == "Program[]"

    def test_function_type(self):
        p = PropSpec.parse("onSelect:fn(id:n)->void")
        assert p.is_function is True
        assert p.type == "fn(id:n)->void"

    def test_no_type(self):
        p = PropSpec.parse("name")
        assert p.type == "any"


# ── Parser: !:fn ─────────────────────────────────────────────────────

class TestParseFn:
    def test_basic_fn(self):
        fn = parse_line("!:fn DashboardPage /props:(programs:Program[] onSelect:fn(id:n)->void)")
        assert isinstance(fn, FnContract)
        assert fn.name == "DashboardPage"
        assert len(fn.props) == 2
        assert fn.prop_names == {"programs", "onSelect"}

    def test_fn_with_renders(self):
        fn = parse_line("!:fn DashboardPage /props:(data:s) /renders:(stats-grid,cards)")
        assert fn.renders == ["stats-grid", "cards"]

    def test_fn_with_returns(self):
        fn = parse_line("!:fn GetUser /props:(id:n) /returns:User")
        assert fn.returns == "User"

    def test_fn_with_throws(self):
        fn = parse_line("!:fn Fetch /props:(url:s) /throws:NetworkError")
        assert fn.throws == "NetworkError"

    def test_fn_required_props(self):
        fn = parse_line("!:fn Form /props:(name:s* email:s* age:n?)")
        assert fn.required_props == {"name", "email"}

    def test_fn_callback_props(self):
        fn = parse_line("!:fn Button /props:(label:s onClick:fn onHover:fn?)")
        assert fn.callback_props == {"onClick", "onHover"}

    def test_sync_fn(self):
        fn = parse_line("!?:fn Loader /props:(id:n) /returns:Data")
        assert isinstance(fn, FnContract)
        assert fn.name == "Loader"


# ── Parser: !:type ───────────────────────────────────────────────────

class TestParseType:
    def test_basic_type(self):
        t = parse_line("!:type Program { id:n name:s* weeks:n level:s exercises:s[] }")
        assert isinstance(t, TypeContract)
        assert t.name == "Program"
        assert len(t.fields) == 5
        assert t.field_names == {"id", "name", "weeks", "level", "exercises"}

    def test_required_field(self):
        t = parse_line("!:type User { name:s* email:s* }")
        name_field = next(f for f in t.fields if f.name == "name")
        assert name_field.required is True

    def test_optional_field(self):
        t = parse_line("!:type Config { debug:b? verbose:b? }")
        debug_field = next(f for f in t.fields if f.name == "debug")
        assert debug_field.optional is True

    def test_type_no_fields(self):
        t = parse_line("!:type Empty")
        assert isinstance(t, TypeContract)
        assert t.name == "Empty"
        assert len(t.fields) == 0


# ── Parser: !:wire ───────────────────────────────────────────────────

class TestParseWire:
    def test_basic_wire(self):
        w = parse_line("!:wire dashboard.onSelect -> programs.highlight")
        assert isinstance(w, WireContract)
        assert w.source == "dashboard.onSelect"
        assert w.target == "programs.highlight"
        assert w.source_component == "dashboard"
        assert w.source_port == "onSelect"
        assert w.target_component == "programs"
        assert w.target_port == "highlight"

    def test_chained_wire(self):
        contracts = parse_contract("!:wire A -> B -> C")
        assert len(contracts.wires) == 2
        assert contracts.wires[0].source == "A"
        assert contracts.wires[0].target == "B"
        assert contracts.wires[1].source == "B"
        assert contracts.wires[1].target == "C"

    def test_wire_no_port(self):
        w = parse_line("!:wire auth -> dashboard")
        assert w.source_component == "auth"
        assert w.source_port == ""
        assert w.target_component == "dashboard"


# ── Parser: !:def ────────────────────────────────────────────────────

class TestParseDef:
    def test_basic_def(self):
        d = parse_line("!:def app /pages:(dashboard,workout,progress)")
        assert isinstance(d, DefContract)
        assert d.name == "app"
        assert d.pages == ["dashboard", "workout", "progress"]

    def test_def_with_deps(self):
        d = parse_line("!:def app /deps:(react,tailwind) /exports:(App)")
        assert d.deps == ["react", "tailwind"]
        assert d.exports == ["App"]

    def test_def_with_types(self):
        d = parse_line("!:def models /types:(User,Post,Comment)")
        assert d.types == ["User", "Post", "Comment"]


# ── Parser: !:api ────────────────────────────────────────────────────

class TestParseApi:
    def test_basic_api(self):
        a = parse_line("!:api POST /users /body:CreateUser /returns:User")
        assert isinstance(a, ApiContract)
        assert a.method == "POST"
        assert a.path == "/users"
        assert a.body == "CreateUser"
        assert a.returns == "User"

    def test_api_with_params(self):
        a = parse_line("!:api GET /users /params:(id:n name:s?)")
        assert a.method == "GET"
        assert len(a.params) == 2

    def test_api_with_status(self):
        a = parse_line("!:api DELETE /users /status:(200,404)")
        assert a.status_codes == [200, 404]


# ── Parser: !:schema ─────────────────────────────────────────────────

class TestParseSchema:
    def test_basic_schema(self):
        s = parse_line("!:schema users { id:n pk email:s* unique name:s }")
        assert isinstance(s, SchemaContract)
        assert s.name == "users"
        assert len(s.fields) == 3

    def test_schema_with_fk(self):
        s = parse_line("!:schema orders { id:n pk user_id:n fk:users total:n }")
        user_id = next(f for f in s.fields if f.name == "user_id")
        assert "fk:users" in user_id.constraints


# ── Parser: multiline and seq() ──────────────────────────────────────

class TestParseMultiline:
    def test_multiline(self):
        contracts = parse_contract("""
            !:fn DashboardPage /props:(data:s)
            !:fn WorkoutPage /props:(workout:Workout)
            !:type Workout { id:n day:n }
            !:wire dashboard.onSelect -> workout.load
        """)
        assert len(contracts.functions) == 2
        assert len(contracts.types) == 1
        assert len(contracts.wires) == 1

    def test_seq_wrapper(self):
        contracts = parse_contract(
            "seq(!:fn A /props:(x:n); !:fn B /props:(y:s); !:wire A -> B)"
        )
        assert len(contracts.functions) == 2
        assert len(contracts.wires) == 1

    def test_ignores_non_code_lines(self):
        contracts = parse_contract("""
            some random text
            !:fn Valid /props:(x:n)
            another line
        """)
        assert len(contracts.functions) == 1

    def test_empty_input(self):
        contracts = parse_contract("")
        assert len(contracts.functions) == 0


# ── Full Stratophic-style contract ───────────────────────────────────

class TestStratophicContract:
    """Parse the exact contract from the Stratophic case study."""

    @pytest.fixture
    def contracts(self):
        return parse_contract("""
            !:def app /name:'FitnessApp' /pages:(dashboard,programs,workout,progress)

            !:type Program { id:n name:s* weeks:n level:s exercises:s[] }
            !:type Workout { id:n programId:n day:n exercises:Exercise[] completed:b }
            !:type Exercise { name:s sets:n reps:n restSec:n }

            !:fn DashboardPage /props:(programs:Program[] onSelect:fn(id:n)->void) /renders:(stats-grid,program-cards)
            !:fn ProgramsPage /props:(programs:Program[] onStart:fn(id:n)->void) /renders:(filter-bar,program-list)
            !:fn WorkoutPage /props:(workout:Workout onComplete:fn(id:n)->void) /renders:(exercise-checklist,timer)
            !:fn ProgressPage /props:(history:Workout[]) /renders:(chart,timeline,streak-counter)

            !:wire dashboard.onSelect -> programs.highlight
            !:wire programs.onStart -> workout.load
            !:wire workout.onComplete -> progress.add
        """)

    def test_module_parsed(self, contracts):
        assert "app" in contracts.modules
        assert contracts.modules["app"].pages == ["dashboard", "programs", "workout", "progress"]

    def test_types_parsed(self, contracts):
        assert contracts.all_type_names == {"Program", "Workout", "Exercise"}

    def test_functions_parsed(self, contracts):
        assert contracts.all_component_names == {
            "DashboardPage", "ProgramsPage", "WorkoutPage", "ProgressPage"
        }

    def test_wires_parsed(self, contracts):
        assert len(contracts.wires) == 3

    def test_dashboard_contract(self, contracts):
        fn = contracts.get_fn("DashboardPage")
        assert fn.prop_names == {"programs", "onSelect"}
        assert fn.callback_props == {"onSelect"}
        assert fn.renders == ["stats-grid", "program-cards"]

    def test_workout_type(self, contracts):
        t = contracts.get_type("Workout")
        assert t.field_names == {"id", "programId", "day", "exercises", "completed"}


# ── Validator: check() ───────────────────────────────────────────────

class TestCheck:
    @pytest.fixture
    def contracts(self):
        return parse_contract("""
            !:fn DashboardPage /props:(programs:Program[] onSelect:fn(id:n)->void)
            !:fn WorkoutPage /props:(workout:Workout onComplete:fn(id:n)->void)
            !:type Program { id:n name:s* weeks:n }
            !:type Workout { id:n day:n }
        """)

    def test_pass_all_props(self, contracts):
        result = check("DashboardPage", props=["programs", "onSelect"], contracts=contracts)
        assert result.passed is True
        assert result.missing_props == []
        assert result.extra_props == []
        assert result.confidence == "!"

    def test_fail_missing_prop(self, contracts):
        result = check("DashboardPage", props=["programs"], contracts=contracts)
        assert result.passed is False
        assert "onSelect" in result.missing_props

    def test_fail_missing_all_props(self, contracts):
        result = check("WorkoutPage", props=[], contracts=contracts)
        assert result.passed is False
        assert "workout" in result.missing_props
        assert "onComplete" in result.missing_props
        assert result.confidence == "~~"

    def test_extra_props_still_pass(self, contracts):
        result = check("DashboardPage", props=["programs", "onSelect", "theme"], contracts=contracts)
        assert result.passed is True
        assert "theme" in result.extra_props

    def test_unknown_component(self, contracts):
        result = check("NonExistent", props=["x"], contracts=contracts)
        assert result.passed is False
        assert "No contract found" in result.issues[0]

    def test_no_props_provided(self, contracts):
        result = check("DashboardPage", contracts=contracts)
        assert result.passed is False

    def test_callbacks_as_separate_arg(self, contracts):
        result = check("DashboardPage", props=["programs"], callbacks=["onSelect"], contracts=contracts)
        assert result.passed is True

    def test_type_reference_warning(self):
        contracts = parse_contract("""
            !:fn Widget /props:(data:UnknownType[])
        """)
        result = check("Widget", props=["data"], contracts=contracts)
        assert len(result.type_mismatches) > 0
        assert "UnknownType" in result.type_mismatches[0]


class TestCheckResult:
    def test_to_condensa_pass(self):
        r = CheckResult(passed=True, component="Dashboard", found_props=["a", "b"])
        s = r.to_condensa()
        assert s.startswith("=:check pass")
        assert "props:(a,b)" in s

    def test_to_condensa_fail(self):
        r = CheckResult(
            passed=False, component="Dashboard",
            found_props=["a"], missing_props=["b"],
            missing_callbacks=["onClick"],
        )
        s = r.to_condensa()
        assert s.startswith("=:check fail")
        assert "missing:(b)" in s
        assert "~~" in s

    def test_str_is_condensa(self):
        r = CheckResult(passed=True, component="X", found_props=["x"])
        assert str(r) == r.to_condensa()


# ── Validator: validate_wires() ──────────────────────────────────────

class TestValidateWires:
    def test_valid_wires(self):
        contracts = parse_contract("""
            !:fn Dashboard /props:(onSelect:fn)
            !:fn Programs /props:(highlight:fn)
            !:wire Dashboard.onSelect -> Programs.highlight
        """)
        issues = validate_wires(contracts)
        assert issues == []

    def test_missing_source_component(self):
        contracts = parse_contract("""
            !:fn Programs /props:(highlight:fn)
            !:wire Unknown.click -> Programs.highlight
        """)
        issues = validate_wires(contracts)
        assert len(issues) == 1
        assert "Unknown" in issues[0]

    def test_missing_target_component(self):
        contracts = parse_contract("""
            !:fn Dashboard /props:(onSelect:fn)
            !:wire Dashboard.onSelect -> Missing.handler
        """)
        issues = validate_wires(contracts)
        assert len(issues) == 1
        assert "Missing" in issues[0]

    def test_missing_source_port(self):
        contracts = parse_contract("""
            !:fn Dashboard /props:(onSelect:fn)
            !:fn Programs /props:(highlight:fn)
            !:wire Dashboard.onClick -> Programs.highlight
        """)
        issues = validate_wires(contracts)
        assert len(issues) == 1
        assert "onClick" in issues[0]

    def test_module_ref_in_wire(self):
        """Module names in wires should not trigger 'not found'."""
        contracts = parse_contract("""
            !:def app /pages:(dashboard)
            !:fn Dashboard /props:(onSelect:fn)
            !:wire app.nav -> Dashboard.onSelect
        """)
        issues = validate_wires(contracts)
        # app is a module, not a function, but should not be flagged
        source_issues = [i for i in issues if "'app' not found" in i]
        assert source_issues == []


# ── Validator: validate_all() ────────────────────────────────────────

class TestValidateAll:
    def test_all_pass(self):
        contracts = parse_contract("""
            !:fn Dashboard /props:(data:s[] onClick:fn)
            !:fn Sidebar /props:(items:s[])
            !:type Item { id:n name:s }
            !:wire Dashboard.onClick -> Sidebar.items
        """)
        result = validate_all(contracts, built_components={
            "Dashboard": ["data", "onClick"],
            "Sidebar": ["items"],
        })
        assert result["passed"] is True
        assert "pass" in result["summary"]

    def test_partial_fail(self):
        contracts = parse_contract("""
            !:fn Dashboard /props:(data:s[] onClick:fn)
            !:fn Sidebar /props:(items:s[])
        """)
        result = validate_all(contracts, built_components={
            "Dashboard": ["data"],  # Missing onClick
            "Sidebar": ["items"],
        })
        assert result["passed"] is False
        assert len(result["component_checks"]) == 2

    def test_type_reference_issues(self):
        contracts = parse_contract("""
            !:fn Widget /props:(data:MissingType[])
        """)
        result = validate_all(contracts)
        assert len(result["type_issues"]) > 0

    def test_no_built_components(self):
        contracts = parse_contract("""
            !:fn A /props:(x:n)
            !:fn B /props:(y:n)
            !:wire A -> B
        """)
        result = validate_all(contracts)
        assert result["passed"] is True  # No component checks, only structural

    def test_summary_format(self):
        contracts = parse_contract("!:fn A /props:(x:n)")
        result = validate_all(contracts, built_components={"A": ["x"]})
        assert result["summary"].startswith("=:check")


# ── ContractSet ──────────────────────────────────────────────────────

class TestContractSet:
    def test_get_fn(self):
        cs = parse_contract("!:fn Foo /props:(x:n)")
        assert cs.get_fn("Foo") is not None
        assert cs.get_fn("Bar") is None

    def test_get_type(self):
        cs = parse_contract("!:type Foo { x:n }")
        assert cs.get_type("Foo") is not None
        assert cs.get_type("Bar") is None

    def test_wired_components(self):
        cs = parse_contract("!:wire A.x -> B.y")
        assert cs.wired_components == {"A", "B"}

    def test_all_component_names(self):
        cs = parse_contract("!:fn A /props:(x:n)\n!:fn B /props:(y:n)")
        assert cs.all_component_names == {"A", "B"}
