import base64
import json
import os
import socket
import unittest


PORT = int(os.environ.get("FX_AGENT_PORT", "9810"))


def token(value: str) -> str:
    if value == "*":
        return "*"
    if value == "":
        return "."
    return base64.urlsafe_b64encode(value.encode("utf-8")).decode("ascii").rstrip("=")


def send(command: str) -> str:
    with socket.create_connection(("127.0.0.1", PORT), timeout=5) as sock:
        sock.sendall((command + "\n").encode("utf-8"))
        sock.shutdown(socket.SHUT_WR)
        chunks = []
        while True:
            block = sock.recv(65536)
            if not block:
                break
            chunks.append(block)
    return b"".join(chunks).decode("utf-8").strip()


class AgentV5LiveTests(unittest.TestCase):
    def test_fxstate_returns_compact_semantic_json(self):
        payload = json.loads(send("FXSTATE"))
        self.assertEqual(payload["status"], "OK")
        self.assertEqual(payload["panelCount"], 1)
        panel = payload["panels"][0]
        self.assertEqual(panel["rootClass"], "javafx.scene.layout.VBox")
        self.assertLess(len(json.dumps(payload)), 20000)
        ids = {control.get("id") for control in panel["controls"]}
        self.assertIn("MyFxTextField", ids)
        table = next(t for t in panel["tables"] if t["id"] == "AdjustmentTable")
        self.assertEqual(table["rowCount"], 2)
        self.assertEqual(table["columns"], ["Code", "Amount"])
        self.assertEqual(table["rows"][0], ["100", "-59.00"])

    def test_fxfind_scopes_exactly_to_panel_class_and_node_identity(self):
        command = "FXFIND {} 0 {} {} * 10".format(
            token("javafx.scene.layout.VBox"),
            token("javafx.scene.control.TextField"),
            token("MyFxTextField"),
        )
        payload = json.loads(send(command))
        self.assertEqual(payload["status"], "OK")
        self.assertEqual(payload["count"], 1)
        match = payload["matches"][0]
        self.assertEqual(match["class"], "javafx.scene.control.TextField")
        self.assertEqual(match["id"], "MyFxTextField")
        self.assertEqual(match["text"], "initial-value")
        self.assertTrue(match["visible"])
        self.assertTrue(match["editable"])

    def test_fxfind_can_scope_to_unique_nested_module_anchor(self):
        command = "FXFIND {} 0 {} {} {} 10".format(
            token("javafx.scene.control.TabPane"),
            token("javafx.scene.control.Tab"),
            token(""),
            token("TabOne"),
        )
        payload = json.loads(send(command))
        self.assertEqual(payload["status"], "OK")
        self.assertEqual(payload["count"], 1)
        self.assertEqual(payload["matches"][0]["text"], "TabOne")

    def test_fxedittext_uses_expected_old_value_and_verifies_readback(self):
        parts = [
            "FXEDITTEXT",
            token("javafx.scene.layout.VBox"), "0",
            token("javafx.scene.control.TextField"), token("MyFxTextField"), "*", "0",
            token("initial-value"), token("edited value"),
        ]
        restore = parts[:-2] + [token("edited value"), token("initial-value")]
        try:
            payload = json.loads(send(" ".join(parts)))
            self.assertEqual(payload["status"], "OK")
            self.assertEqual(payload["before"], "initial-value")
            self.assertEqual(payload["requested"], "edited value")
            self.assertEqual(payload["actual"], "edited value")
        finally:
            send(" ".join(restore))

    def test_fxtablecell_selects_specific_row_and_column_with_expected_value(self):
        parts = [
            "FXTABLECELL",
            token("javafx.scene.layout.VBox"), "0", token("Amount"), "0",
            "1", "1", "select", token("12.50"),
        ]
        payload = json.loads(send(" ".join(parts)))
        self.assertEqual(payload["status"], "OK")
        self.assertEqual(payload["actual"], "12.50")
        self.assertEqual(payload["selectedRow"], 1)
        self.assertEqual(payload["selectedColumn"], 1)

    def test_fxedittext_rejects_wildcard_expected_value_without_mutating(self):
        base = [
            "FXEDITTEXT", token("javafx.scene.layout.VBox"), "0",
            token("javafx.scene.control.TextField"), token("MyFxTextField"), token("initial-value"), "0",
        ]
        try:
            payload = json.loads(send(" ".join(base + ["*", token("SHOULD_NOT_APPEAR")])))
            self.assertEqual(payload["status"], "ERROR")
            self.assertIn("wildcard", payload["message"].lower())
        finally:
            # Clean up even against the deliberately unsafe RED implementation.
            send(" ".join([
                "FXEDITTEXT", token("javafx.scene.layout.VBox"), "0",
                token("javafx.scene.control.TextField"), token("MyFxTextField"), token("SHOULD_NOT_APPEAR"), "0",
                token("SHOULD_NOT_APPEAR"), token("initial-value"),
            ]))

    def test_fxedittext_rejects_wildcard_panel_scope(self):
        payload = json.loads(send(" ".join([
            "FXEDITTEXT", "*", "0", token("javafx.scene.control.TextField"),
            token("MyFxTextField"), token("initial-value"), "0",
            token("initial-value"), token("SHOULD_NOT_APPEAR"),
        ])))
        if payload.get("status") == "OK":
            send(" ".join([
                "FXEDITTEXT", token("javafx.scene.layout.VBox"), "0",
                token("javafx.scene.control.TextField"), token("MyFxTextField"), token("SHOULD_NOT_APPEAR"), "0",
                token("SHOULD_NOT_APPEAR"), token("initial-value"),
            ]))
        self.assertEqual(payload["status"], "ERROR")

    def test_fxtablecell_rejects_wildcard_expected_value(self):
        payload = json.loads(send(" ".join([
            "FXTABLECELL", token("javafx.scene.layout.VBox"), "0", token("Amount"), "0",
            "0", "1", "select", "*",
        ])))
        self.assertEqual(payload["status"], "ERROR")
        self.assertIn("wildcard", payload["message"].lower())

    def test_legacy_fx_mutation_command_is_disabled_with_structured_error(self):
        payload = json.loads(send("FXSETTEXT missing-selector blocked"))
        self.assertEqual(payload["status"], "ERROR")
        self.assertIn("disabled", payload["message"].lower())

    def test_mutation_rejects_ambiguous_nested_panel_anchor(self):
        payload = json.loads(send(" ".join([
            "FXEDITTEXT", token("javafx.scene.control.TextField"), "0",
            token("javafx.scene.control.TextField"), token("MyFxTextField"), token("initial-value"), "0",
            token("initial-value"), token("never-written"),
        ])))
        self.assertEqual(payload["status"], "ERROR")
        self.assertIn("uniquely", payload["message"].lower())

    def test_malformed_protocol_input_returns_structured_json_error(self):
        payload = json.loads(send("FXFIND * not-an-integer * * * 10"))
        self.assertEqual(payload["status"], "ERROR")

    def test_fxtablecell_doubleclick_fires_only_on_resolved_cell(self):
        parts = [
            "FXTABLECELL",
            token("javafx.scene.layout.VBox"), "0", token("Code"), "0",
            "0", "0", "doubleclick", token("100"),
        ]
        payload = json.loads(send(" ".join(parts)))
        self.assertEqual(payload["status"], "OK")
        find_status = "FXFIND {} 0 {} {} * 10".format(
            token("javafx.scene.layout.VBox"), token("javafx.scene.control.Label"), token("StatusLabel"))
        status = json.loads(send(find_status))["matches"][0]["text"]
        self.assertEqual(status, "status: table doubleclick row=0")


if __name__ == "__main__":
    unittest.main()
