"""Core tests for cdn-ai package."""

import pytest
from cdn import encode, decode, encode_with_stats, count_tokens, tokenize
from cdn import CondensaEncoder, CondensaDecoder


class TestEncode:
    """Test NL → Condensa encoding."""

    def test_basic_search(self):
        result = encode("Search the web for SpaceX news")
        assert "srch" in result
        assert result.startswith("!")

    def test_filter(self):
        result = encode("Filter active users and sort descending")
        assert "filt" in result

    def test_error(self):
        result = encode("Error: the API returned a 429")
        assert result.startswith("E:")
        assert "429" in result

    def test_cancel(self):
        result = encode("Cancel all running tasks and discard partial results")
        assert result.startswith("X:")
        assert "discard" in result.lower() or "discard:T" in result

    def test_status(self):
        result = encode("Processing complete. 4 out of 4 stages done.")
        assert "#:" in result
        assert "done" in result

    def test_workflow(self):
        result = encode(
            "First, read the data. Then, filter active records. "
            "After that, group by region."
        )
        assert "seq(" in result or "|" in result

    def test_urgency_critical(self):
        result = encode("CRITICAL: drop everything and execute shutdown")
        assert "+++" in result

    def test_urgency_high(self):
        result = encode("Urgent: search for security patches immediately")
        assert "++" in result

    def test_encode_returns_string(self):
        result = encode("Do something")
        assert isinstance(result, str)
        assert len(result) > 0


class TestDecode:
    """Test Condensa → NL decoding."""

    def test_command(self):
        result = decode("!:srch 'SpaceX' /n:5")
        assert "search" in result.lower() or "srch" in result.lower()

    def test_sync_command_v03(self):
        result = decode("!?:read user_profile /timeout:10s fb:cache")
        assert "synchronous" in result.lower() or "sync" in result.lower()

    def test_sync_command_v02_compat(self):
        result = decode("!!:read user_profile /timeout:10s")
        assert "synchronous" in result.lower() or "sync" in result.lower()

    def test_error(self):
        result = decode("E:429 retry:3 wait:30s backoff:exp fb:degrade")
        assert "429" in result
        assert "retry" in result.lower()

    def test_status_done(self):
        result = decode("#:done 4/4 records:12847 fail:0")
        assert "completed" in result.lower() or "done" in result.lower()

    def test_cancel(self):
        result = decode("X: all discard:T")
        assert "cancel" in result.lower()

    def test_update(self):
        result = decode("~:^ s1:keep s2:revise+detail s3:del")
        assert "keep" in result.lower()
        assert "revise" in result.lower()

    def test_pipeline(self):
        result = decode("!:filt status:active | grp region | avg revenue")
        assert "pipeline" in result.lower() or "→" in result

    def test_pipeline_bare_verbs(self):
        """v0.2+ implicit verb mode — bare verbs after first step."""
        result = decode("!:filt active | grp region | avg revenue | sort /desc")
        assert "pipeline" in result.lower() or "→" in result

    def test_policy(self):
        result = decode("@:policy retry:3 timeout:30s fb:degrade")
        assert "session" in result.lower() or "policy" in result.lower() or "default" in result.lower()

    def test_meta(self):
        result = decode("@:define QRS 'read|filt|grp|agg'")
        assert "define" in result.lower()

    def test_delegate(self):
        result = decode(">:@AgentC review code")
        assert "delegate" in result.lower()

    def test_result_with_confidence(self):
        result = decode("=:42 !")
        assert "42" in result

    def test_urgency_prefix(self):
        result = decode("+++!:exec shutdown")
        assert "critical" in result.lower()

    def test_modifiers(self):
        result = decode("!:srch 'query' /n:5 /fmt:json /since:7d")
        assert "5" in result
        assert "json" in result.lower()

    def test_fallback(self):
        result = decode("!:read data fb:cache")
        assert "cache" in result.lower()

    def test_each_modifier_v03(self):
        """v0.3 /each modifier."""
        result = decode("!:sumz results /each")
        assert "each" in result.lower()

    def test_decode_returns_string(self):
        result = decode("!:srch test")
        assert isinstance(result, str)


class TestEncodeWithStats:
    """Test encoding with compression statistics."""

    def test_returns_dict(self):
        result = encode_with_stats("Search for SpaceX news")
        assert isinstance(result, dict)

    def test_has_required_keys(self):
        result = encode_with_stats("Search for SpaceX news")
        assert "original" in result
        assert "condensa" in result
        assert "nl_tokens" in result
        assert "condensa_tokens" in result
        assert "reduction_pct" in result

    def test_compression_positive(self):
        result = encode_with_stats(
            "Please search the web for the latest news about SpaceX Starship "
            "and give me a summary of the top 5 results"
        )
        assert result["reduction_pct"] > 0

    def test_token_counts_positive(self):
        result = encode_with_stats("Search for something")
        assert result["nl_tokens"] > 0
        assert result["condensa_tokens"] > 0


class TestUtils:
    """Test utility functions."""

    def test_count_tokens(self):
        n = count_tokens("hello world")
        assert isinstance(n, int)
        assert n > 0

    def test_count_tokens_empty(self):
        assert count_tokens("") == 0

    def test_tokenize(self):
        tokens = tokenize("hello world")
        assert isinstance(tokens, list)
        assert len(tokens) > 0
        assert all(isinstance(t, str) for t in tokens)


class TestRoundTrip:
    """Test encode → decode round-trip preserves meaning."""

    @pytest.mark.parametrize("condensa_msg", [
        "!:srch 'SpaceX' /n:5",
        "E:429 retry:3 fb:degrade",
        "#:done 4/4 records:100",
        "X: all discard:T",
        "~:^ s1:keep s2:del",
        "@:policy retry:3 timeout:30s",
    ])
    def test_decode_does_not_crash(self, condensa_msg):
        """Every valid Condensa message should decode without error."""
        result = decode(condensa_msg)
        assert isinstance(result, str)
        assert len(result) > 0


class TestBackwardCompatibility:
    """Ensure v0.1 and v0.2 syntax still works."""

    def test_v01_full_pipeline(self):
        """v0.1: explicit !: on every step."""
        result = decode("!:filt active | !:grp region | !:avg revenue")
        assert "pipeline" in result.lower() or "→" in result

    def test_v02_sync_bang_bang(self):
        """v0.2: !! sync command still decodes."""
        result = decode("!!:read profile /timeout:5s")
        assert "synchronous" in result.lower() or "sync" in result.lower()

    def test_v02_fallback_fb(self):
        """fb: fallback works across all versions."""
        result = decode("!:read data fb:cache")
        assert "cache" in result.lower()


# ═══════════════════════════════════════════════════════════════════
# New tests below — 79 additional tests across 8 new test classes
# ═══════════════════════════════════════════════════════════════════


class TestEncodeNewVerbs:
    """Test the new action verbs added in the encoder."""

    def test_encode_delegation(self):
        result = encode("Delegate to AgentC: review code")
        assert ">:" in result, f"Expected delegation prefix '>:' in '{result}'"

    def test_encode_review(self):
        result = encode("Review the pull request")
        assert "review" in result.lower(), f"Expected 'review' in '{result}'"

    def test_encode_create(self):
        result = encode("Create a new user account")
        assert "crt" in result, f"Expected 'crt' in '{result}'"

    def test_encode_deploy(self):
        result = encode("Deploy to production")
        assert "deploy" in result, f"Expected 'deploy' in '{result}'"

    def test_encode_monitor(self):
        result = encode("Monitor CPU usage")
        assert "mon" in result, f"Expected 'mon' in '{result}'"

    def test_encode_schedule(self):
        result = encode("Schedule a backup")
        assert "sched" in result, f"Expected 'sched' in '{result}'"

    def test_encode_notify(self):
        result = encode("Notify the team")
        assert "ntfy" in result, f"Expected 'ntfy' in '{result}'"

    def test_encode_approve(self):
        result = encode("Approve the request")
        assert "aprv" in result, f"Expected 'aprv' in '{result}'"

    def test_encode_build(self):
        result = encode("Build the Docker image")
        assert "build" in result, f"Expected 'build' in '{result}'"

    def test_encode_scan(self):
        result = encode("Scan for vulnerabilities")
        assert "scan" in result, f"Expected 'scan' in '{result}'"


class TestEncodeNewFeatures:
    """Test new encoding features: results, ack, pipeline, policy, etc."""

    def test_encode_result(self):
        result = encode("Here are the results: 3 matches found")
        assert result.startswith("=:"), f"Expected result to start with '=:' but got '{result}'"

    def test_encode_acknowledge(self):
        result = encode("Acknowledged")
        assert "ack" in result.lower() or "<:" in result, \
            f"Expected 'ack' or '<:' in '{result}'"

    def test_encode_pipeline_and_then(self):
        result = encode("Search for X and then summarize")
        assert "|" in result, f"Expected '|' pipeline operator in '{result}'"

    def test_encode_policy(self):
        result = encode("Set default retry to 3 for all messages")
        assert "@:policy" in result, f"Expected '@:policy' in '{result}'"

    def test_encode_context_update(self):
        result = encode("Language changed from English to Spanish")
        assert "~:" in result, f"Expected '~:' context update in '{result}'"

    def test_encode_correction(self):
        result = encode("Keep section 1, revise section 2, delete section 3")
        assert "~:^" in result, f"Expected '~:^' correction prefix in '{result}'"

    def test_encode_parallel(self):
        result = encode("Execute three tasks in parallel: 1) search news 2) translate text 3) summarize results")
        assert "par(" in result, f"Expected 'par(' in '{result}'"

    def test_encode_sync(self):
        result = encode("Read the data synchronously and return the result")
        assert "!?:" in result, f"Expected '!?:' sync prefix in '{result}'"


class TestDecodeAllTypes:
    """Test decoding every message type."""

    def test_decode_command_basic(self):
        result = decode("!:srch test")
        assert isinstance(result, str)
        assert len(result) > 0

    def test_decode_sync(self):
        result = decode("!?:read data /timeout:5s")
        assert "synchronous" in result.lower() or "sync" in result.lower()

    def test_decode_query(self):
        result = decode("?:cnt users")
        assert "count" in result.lower() or "cnt" in result.lower()

    def test_decode_result(self):
        result = decode("=:42")
        assert "42" in result

    def test_decode_delegate_with_agent(self):
        result = decode(">:@AgentC review code")
        assert "delegate" in result.lower()

    def test_decode_receive(self):
        result = decode("<:ack")
        assert isinstance(result, str)
        assert len(result) > 0

    def test_decode_update_inherit(self):
        result = decode("~:^ s1:keep s2:del")
        assert "keep" in result.lower()

    def test_decode_update_extend(self):
        result = decode("~:& lang:en->es")
        assert "lang" in result.lower() or "en" in result.lower()

    def test_decode_status_run(self):
        result = decode("#:run 3/5 records:1000")
        assert "progress" in result.lower() or "3" in result

    def test_decode_cancel_all(self):
        result = decode("X: all discard:T")
        assert "cancel" in result.lower()

    def test_decode_error_with_fallback(self):
        result = decode("E:timeout fb:retry:3")
        assert "timeout" in result.lower()

    def test_decode_meta_define(self):
        result = decode("@:define QRS 'read|filt|grp'")
        assert "define" in result.lower()


class TestDecodeAdvanced:
    """Test advanced decoding: seq, par, pipelines, urgency, etc."""

    def test_decode_seq(self):
        result = decode("seq(!:read data; filt active; grp region)")
        assert "sequential" in result.lower(), f"Expected 'sequential' in '{result}'"

    def test_decode_par(self):
        result = decode("par(!:xlat /lang:es; !:sumz /n:200)")
        assert "parallel" in result.lower(), f"Expected 'parallel' in '{result}'"

    def test_decode_deep_pipeline(self):
        result = decode("!:filt active | grp region | avg revenue | sort /desc | fmt /fmt:json")
        assert "pipeline" in result.lower() or "\u2192" in result, \
            f"Expected 'pipeline' or arrow in '{result}'"

    def test_decode_bulk_literal(self):
        result = decode('!:wrt <<<{"key": "value"}>>>')
        assert "key" in result or "value" in result, \
            f"Expected bulk content in '{result}'"

    def test_decode_urgency_critical(self):
        result = decode("+++!:exec shutdown")
        assert "CRITICAL" in result, f"Expected 'CRITICAL' in '{result}'"

    def test_decode_urgency_background(self):
        result = decode("--!:exec cleanup")
        assert "BACKGROUND" in result, f"Expected 'BACKGROUND' in '{result}'"

    def test_decode_on_error_routing(self):
        result = decode("@:on E:429 fb:degrade")
        assert isinstance(result, str)
        assert len(result) > 0

    def test_decode_confidence_markers(self):
        r1 = decode("=:value ~")
        r2 = decode("=:value ~~")
        r3 = decode("=:value ???")
        assert "likely" in r1.lower() or "uncertain" in r1.lower() or "value" in r1
        assert "uncertain" in r2.lower() or "value" in r2
        assert "guess" in r3.lower() or "value" in r3


class TestDecodeEdgeCases:
    """Test decoder edge cases: empty, unicode, special chars, etc."""

    def test_decode_empty_string(self):
        result = decode("")
        assert isinstance(result, str), "decode('') should return a string"

    def test_decode_just_type(self):
        result = decode("!:")
        assert isinstance(result, str), "decode('!:') should return a string"

    def test_decode_no_colon(self):
        result = decode("hello world")
        assert "unparseable" in result.lower(), \
            f"Expected '[unparseable: ...]' for non-Condensa input, got '{result}'"

    def test_decode_single_char(self):
        result = decode("!")
        assert isinstance(result, str), "decode('!') should return a string"

    def test_decode_unicode_param(self):
        result = decode("!:srch '\u65e5\u672c\u8a9e\u30c6\u30b9\u30c8'")
        assert "\u65e5\u672c\u8a9e" in result, f"Expected unicode text in '{result}'"

    def test_decode_very_long_message(self):
        result = decode("!:srch " + "x" * 500)
        assert isinstance(result, str)
        assert len(result) > 0

    def test_decode_special_chars(self):
        result = decode("!:srch '@#$%'")
        assert isinstance(result, str)

    def test_decode_nested_parens(self):
        result = decode("seq(!:a; par(!:b; !:c))")
        assert isinstance(result, str)
        assert len(result) > 0

    def test_decode_numbers_only(self):
        result = decode("=:42")
        assert "42" in result

    def test_decode_multiple_modifiers(self):
        result = decode("!:srch query /n:5 /fmt:json /since:7d /desc")
        assert "5" in result
        assert "json" in result.lower()
        assert "7d" in result or "7" in result
        assert "descending" in result.lower() or "desc" in result.lower()

    def test_decode_fallback_retry(self):
        result = decode("!:read data fb:retry:3")
        assert "retry" in result.lower(), f"Expected 'retry' in '{result}'"

    def test_decode_ref_dollar(self):
        result = decode("!:sumz $_ /n:200")
        assert isinstance(result, str)
        assert len(result) > 0

    def test_decode_ref_numbered(self):
        result = decode("!:sumz $1 /n:200")
        assert isinstance(result, str)
        assert len(result) > 0

    def test_decode_context_inherit_n(self):
        result = decode("^3:sumz /fmt:bullets")
        assert isinstance(result, str)
        assert len(result) > 0

    def test_decode_unknown_verb(self):
        result = decode("!:xyzabc test")
        assert isinstance(result, str)
        assert "xyzabc" in result.lower() or len(result) > 0


class TestEncodeEdgeCases:
    """Test encoder edge cases: empty, unicode, special chars, etc."""

    def test_encode_empty(self):
        result = encode("")
        assert isinstance(result, str), "encode('') should return a string"

    def test_encode_single_word(self):
        result = encode("hello")
        assert isinstance(result, str)

    def test_encode_unicode(self):
        result = encode("\u641c\u7d22\u4eba\u5de5\u667a\u80fd")
        assert isinstance(result, str)

    def test_encode_already_condensa(self):
        result = encode("!:srch test /n:5")
        assert isinstance(result, str)

    def test_encode_very_long(self):
        result = encode("a " * 200)
        assert isinstance(result, str)

    def test_encode_special_chars(self):
        result = encode("!@#$%^&*()")
        assert isinstance(result, str)

    def test_encode_newlines(self):
        result = encode("line1\nline2\nline3")
        assert isinstance(result, str)

    def test_encode_json_input(self):
        result = encode('{"action": "search", "query": "test"}')
        assert isinstance(result, str)


class TestRoundTripExpanded:
    """Test encode->decode round trip for diverse message types."""

    @pytest.mark.parametrize("nl_input", [
        "Search for AI news and return top 5",
        "Error: API returned 503",
        "Cancel all tasks",
        "Task complete. 100 records processed. 0 failures.",
        "Update: language changed to Spanish",
        "Delegate to AgentB: test the code",
        "Filter active, group by region, sort descending",
        "Set default retry 3, timeout 30s",
        "In parallel: translate to Spanish and summarize",
        "Acknowledged",
    ])
    def test_roundtrip(self, nl_input):
        """Encode should not crash, decode of encoded should not crash,
        and some semantic content should survive."""
        encoded = encode(nl_input)
        assert isinstance(encoded, str), f"encode() should return str for '{nl_input}'"
        assert len(encoded) > 0, f"encode() should return non-empty for '{nl_input}'"

        decoded = decode(encoded)
        assert isinstance(decoded, str), f"decode() should return str for '{encoded}'"
        assert len(decoded) > 0, f"decode() should return non-empty for '{encoded}'"


class TestCLI:
    """Test CLI commands using subprocess."""

    def _run_cli(self, *args):
        import subprocess
        import sys
        return subprocess.run(
            [sys.executable, "-m", "cdn.cli", *args],
            capture_output=True, text=True,
            cwd="/Users/worachet/Dev/ai-comm-optimization/condensa",
        )

    def test_cli_version(self):
        r = self._run_cli("version")
        assert "cdn-ai" in r.stdout, f"Expected 'cdn-ai' in version output, got '{r.stdout}'"

    def test_cli_encode(self):
        r = self._run_cli("encode", "Search for news")
        assert len(r.stdout.strip()) > 0, "encode should produce non-empty output"

    def test_cli_decode(self):
        r = self._run_cli("decode", "!:srch news /n:5")
        assert len(r.stdout.strip()) > 0, "decode should produce non-empty output"

    def test_cli_stats(self):
        r = self._run_cli("stats", "Search for news")
        import json
        data = json.loads(r.stdout)
        assert "condensa" in data, "stats output should contain 'condensa' key"

    def test_cli_tokenize(self):
        r = self._run_cli("tokenize", "hello world")
        assert "Tokens" in r.stdout or "tokens" in r.stdout, \
            f"Expected 'Tokens' in output, got '{r.stdout}'"

    def test_cli_no_args(self):
        r = self._run_cli()
        assert r.returncode == 0, f"cdn with no args should exit 0, got {r.returncode}"

    def test_cli_encode_empty(self):
        r = self._run_cli("encode", "")
        assert r.returncode == 0, f"encode '' should not crash, got returncode {r.returncode}"

    def test_cli_decode_complex(self):
        r = self._run_cli("decode", "!:filt active | grp region")
        assert "Pipeline" in r.stdout or "pipeline" in r.stdout, \
            f"Expected 'Pipeline' in output, got '{r.stdout}'"


# ═══════════════════════════════════════════════════════════════════
# Condensa Code tests — v0.4 Code primitives
# ═══════════════════════════════════════════════════════════════════


class TestCondensaCodeDecode:
    """Test decoding Condensa Code primitives."""

    def test_decode_fn(self):
        result = decode("!:fn DashboardPage /props:(programs:Program[] onSelect:fn)")
        assert "function" in result.lower() or "fn" in result.lower() or "dashboard" in result.lower()

    def test_decode_type(self):
        result = decode("!:type Program { id:n name:s* weeks:n }")
        assert "type" in result.lower() or "program" in result.lower()

    def test_decode_def(self):
        result = decode("!:def app /name:'Fitness' /pages:(dashboard,workout)")
        assert "define" in result.lower() or "module" in result.lower()

    def test_decode_wire(self):
        result = decode("!:wire dashboard.onSelect -> programs.highlight")
        assert "wire" in result.lower() or "dashboard" in result.lower()

    def test_decode_api(self):
        result = decode("!:api POST /users /body:CreateUser /returns:User")
        assert "api" in result.lower() or "post" in result.lower()

    def test_decode_schema(self):
        result = decode("!:schema workouts { id:n pk user_id:n fk:users }")
        assert "schema" in result.lower() or "workouts" in result.lower()

    def test_decode_check(self):
        result = decode("?:chk DashboardPage /accepts:(programs:Program[])")
        assert isinstance(result, str) and len(result) > 0

    def test_decode_mod(self):
        result = decode("~:mod app /add:page settings")
        assert isinstance(result, str) and len(result) > 0

    def test_decode_asm(self):
        result = decode("!?:asm /wire:(dashboard->programs->workout)")
        assert "assemble" in result.lower() or "asm" in result.lower()

    def test_decode_wire_chain(self):
        result = decode("!:wire auth.login -> app.setUser -> dashboard.refresh")
        assert "wire" in result.lower() or "auth" in result.lower()


class TestCondensaCodeEncode:
    """Test encoding Condensa Code from NL."""

    def test_encode_component(self):
        result = encode("Define a component DashboardPage that accepts programs")
        assert "fn" in result or "DashboardPage" in result

    def test_encode_wire(self):
        result = encode("Wire dashboard.onSelect to programs.highlight")
        assert "wire" in result

    def test_encode_api_endpoint(self):
        result = encode("Define an API endpoint POST /users that returns User")
        assert "api" in result and "POST" in result

    def test_encode_assemble(self):
        result = encode("Assemble the components")
        assert "asm" in result

    def test_encode_define_module(self):
        result = encode("Define a module for the fitness app")
        assert "def" in result


class TestCondensaCodeRoundTrip:
    """Test Code messages survive encode→decode."""

    @pytest.mark.parametrize("cdn_msg", [
        "!:fn DashboardPage /props:(programs:Program[])",
        "!:type Program { id:n name:s* }",
        "!:def app /pages:(dashboard,workout)",
        "!:wire dashboard.onSelect -> programs.highlight",
        "!:api POST /users /returns:User",
        "!:schema workouts { id:n pk completed:b }",
        "!?:asm",
    ])
    def test_code_roundtrip(self, cdn_msg):
        result = decode(cdn_msg)
        assert isinstance(result, str)
        assert len(result) > 0
