"""Tests for spec compliance fixes — self-referential reassignment, double-recording,
annotated assignments, write detection, format().load(), and enrich changeTypes."""

import json
import unittest
from unittest.mock import patch

from cylint.diff.classifier import extract_operations


class TestSelfReferentialReassignment(unittest.TestCase):
    """Fix #1: df = df.filter(...) must not duplicate operations."""

    def test_reassignment_no_duplicate_filters(self):
        source = '''\
orders = spark.table("orders")
orders = orders.filter("date > '2024'")
orders = orders.filter("status = 'active'")
'''
        ops = extract_operations(source)
        orders_ops = [op for op in ops if op.variable == "orders"]
        self.assertEqual(len(orders_ops), 1)
        # Should have exactly 2 filters, not duplicated
        self.assertEqual(len(orders_ops[0].filters), 2)

    def test_reassignment_preserves_source_table(self):
        source = '''\
orders = spark.table("orders")
orders = orders.filter("date > '2024'")
'''
        ops = extract_operations(source)
        orders_ops = [op for op in ops if op.variable == "orders"]
        self.assertEqual(orders_ops[0].source_table, "orders")

    def test_triple_reassignment(self):
        source = '''\
orders = spark.table("orders")
orders = orders.filter("a > 1")
orders = orders.filter("b > 2")
orders = orders.select("id", "amount")
'''
        ops = extract_operations(source)
        orders_ops = [op for op in ops if op.variable == "orders"]
        self.assertEqual(len(orders_ops), 1)
        self.assertEqual(len(orders_ops[0].filters), 2)
        self.assertEqual(len(orders_ops[0].selects), 1)

    def test_reassignment_with_cache(self):
        source = '''\
orders = spark.table("orders")
orders = orders.cache()
'''
        ops = extract_operations(source)
        orders_ops = [op for op in ops if op.variable == "orders"]
        self.assertEqual(len(orders_ops), 1)
        # Should have exactly 1 cache, not duplicated
        self.assertEqual(len(orders_ops[0].caches), 1)


class TestNoDuplicateCachePersist(unittest.TestCase):
    """Fix #3: Standalone df.cache() must not double-record."""

    def test_standalone_cache_single_record(self):
        source = '''\
orders = spark.table("orders")
orders.cache()
'''
        ops = extract_operations(source)
        orders_ops = [op for op in ops if op.variable == "orders"]
        self.assertEqual(len(orders_ops), 1)
        self.assertEqual(len(orders_ops[0].caches), 1)

    def test_standalone_persist_single_record(self):
        source = '''\
orders = spark.table("orders")
orders.persist()
'''
        ops = extract_operations(source)
        orders_ops = [op for op in ops if op.variable == "orders"]
        self.assertEqual(len(orders_ops[0].caches), 1)


class TestAnnotatedAssignment(unittest.TestCase):
    """Fix #4: Annotated assignments with aliases and method chains."""

    def test_ann_assign_alias(self):
        source = '''\
orders = spark.table("orders")
recent: "DataFrame" = orders
'''
        ops = extract_operations(source)
        recent_ops = [op for op in ops if op.variable == "recent"]
        self.assertEqual(len(recent_ops), 1)
        self.assertEqual(recent_ops[0].source_table, "orders")

    def test_ann_assign_method_chain(self):
        source = '''\
orders = spark.table("orders")
filtered: "DataFrame" = orders.filter("status = 'active'")
'''
        ops = extract_operations(source)
        filtered_ops = [op for op in ops if op.variable == "filtered"]
        self.assertEqual(len(filtered_ops), 1)
        self.assertGreater(len(filtered_ops[0].filters), 0)
        self.assertEqual(filtered_ops[0].source_table, "orders")


class TestWriteDetectionInChain(unittest.TestCase):
    """Fix #6: Write operations must be detected in chain assignments."""

    def test_write_in_assignment_chain(self):
        """df.write.parquet("x") detected as standalone expr."""
        source = '''\
orders = spark.table("orders")
orders.write.parquet("/out/orders")
'''
        ops = extract_operations(source)
        orders_ops = [op for op in ops if op.variable == "orders"]
        self.assertEqual(len(orders_ops), 1)
        self.assertGreater(len(orders_ops[0].writes), 0)
        self.assertEqual(orders_ops[0].writes[0].target, "/out/orders")
        self.assertEqual(orders_ops[0].writes[0].format, "parquet")

    def test_write_save_as_table(self):
        source = '''\
orders = spark.table("orders")
orders.write.saveAsTable("output_table")
'''
        ops = extract_operations(source)
        orders_ops = [op for op in ops if op.variable == "orders"]
        self.assertGreater(len(orders_ops[0].writes), 0)
        self.assertEqual(orders_ops[0].writes[0].target, "output_table")


class TestFormatLoadSourceTable(unittest.TestCase):
    """Fix #8: spark.read.format("csv").load("path") source table extraction."""

    def test_format_load_pattern(self):
        source = 'data = spark.read.format("csv").load("/data/events.csv")\n'
        ops = extract_operations(source)
        self.assertEqual(len(ops), 1)
        self.assertEqual(ops[0].source_table, "/data/events.csv")

    def test_format_load_with_options(self):
        source = 'data = spark.read.format("parquet").option("mergeSchema", "true").load("/data/v2")\n'
        ops = extract_operations(source)
        self.assertEqual(len(ops), 1)
        self.assertEqual(ops[0].source_table, "/data/v2")


class TestEnrichRequestChangeTypes(unittest.TestCase):
    """Fix #2: EnrichRequest must include changeTypes in the payload."""

    def test_change_types_field_exists(self):
        from cylint.ci.enrich import EnrichRequest
        req = EnrichRequest(
            environment="prod",
            change_types=[{"changeType": "filter_removed", "file": "test.py"}],
        )
        self.assertEqual(len(req.change_types), 1)

    def test_post_enrich_includes_change_types(self):
        """Verify that post_enrich sends changeTypes in the JSON body."""
        from cylint.ci.enrich import EnrichRequest

        import json as json_mod
        from http.server import BaseHTTPRequestHandler, HTTPServer
        from threading import Thread

        received_payload = {}

        class Handler(BaseHTTPRequestHandler):
            def do_POST(self):
                nonlocal received_payload
                length = int(self.headers.get("Content-Length", 0))
                received_payload = json_mod.loads(self.rfile.read(length))

                body = json_mod.dumps({
                    "findings": [], "planFindings": [],
                    "changeFindings": [], "matchStats": {},
                }).encode()
                self.send_response(200)
                self.send_header("Content-Type", "application/json")
                self.send_header("Content-Length", str(len(body)))
                self.end_headers()
                self.wfile.write(body)

            def log_message(self, *args):
                pass

        server = HTTPServer(("127.0.0.1", 0), Handler)
        thread = Thread(target=server.serve_forever)
        thread.daemon = True
        thread.start()

        try:
            host, port = server.server_address
            from cylint.ci.enrich import post_enrich
            req = EnrichRequest(
                files=[{"path": "test.py"}],
                linter_findings=[],
                change_types=[{"changeType": "filter_removed", "file": "test.py", "line": 10}],
                environment="prod",
            )
            post_enrich(req, "test-key", base_url=f"http://{host}:{port}")
            self.assertIn("changeTypes", received_payload)
            self.assertEqual(len(received_payload["changeTypes"]), 1)
        finally:
            server.shutdown()

    def test_post_enrich_omits_empty_change_types(self):
        """When no change types, the key should not be in the payload."""
        from cylint.ci.enrich import EnrichRequest

        import json as json_mod
        from http.server import BaseHTTPRequestHandler, HTTPServer
        from threading import Thread

        received_payload = {}

        class Handler(BaseHTTPRequestHandler):
            def do_POST(self):
                nonlocal received_payload
                length = int(self.headers.get("Content-Length", 0))
                received_payload = json_mod.loads(self.rfile.read(length))

                body = json_mod.dumps({
                    "findings": [], "planFindings": [],
                    "changeFindings": [], "matchStats": {},
                }).encode()
                self.send_response(200)
                self.send_header("Content-Type", "application/json")
                self.send_header("Content-Length", str(len(body)))
                self.end_headers()
                self.wfile.write(body)

            def log_message(self, *args):
                pass

        server = HTTPServer(("127.0.0.1", 0), Handler)
        thread = Thread(target=server.serve_forever)
        thread.daemon = True
        thread.start()

        try:
            host, port = server.server_address
            from cylint.ci.enrich import post_enrich
            req = EnrichRequest(
                files=[{"path": "test.py"}],
                linter_findings=[],
                environment="prod",
            )
            post_enrich(req, "test-key", base_url=f"http://{host}:{port}")
            self.assertNotIn("changeTypes", received_payload)
        finally:
            server.shutdown()


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