#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import os
import shutil
import tempfile
import time

from pyspark.errors import PySparkTypeError, PySparkValueError
from pyspark.sql.types import (
    StructType,
    StructField,
    LongType,
    StringType,
    IntegerType,
    ArrayType,
    MapType,
    Row,
)
from pyspark.testing.objects import (
    PythonOnlyUDT,
    ExamplePoint,
    PythonOnlyPoint,
)

from pyspark.testing.connectutils import should_test_connect
from pyspark.sql.tests.connect.test_connect_basic import SparkConnectSQLTestCase

if should_test_connect:
    from pyspark.sql.connect.readwriter import DataFrameWriterV2


class SparkConnectReadWriterTests(SparkConnectSQLTestCase):
    def test_simple_read(self):
        df = self.connect.read.table(self.tbl_name)
        data = df.limit(10).toPandas()
        # Check that the limit is applied
        self.assertEqual(len(data.index), 10)

    def test_json(self):
        with tempfile.TemporaryDirectory(prefix="test_json") as d:
            # Write a DataFrame into a JSON file
            self.spark.createDataFrame([{"age": 100, "name": "Hyukjin Kwon"}]).write.mode(
                "overwrite"
            ).format("json").save(d)
            # Read the JSON file as a DataFrame.
            self.assert_eq(self.connect.read.json(d).toPandas(), self.spark.read.json(d).toPandas())

            for schema in [
                "age INT, name STRING",
                StructType(
                    [
                        StructField("age", IntegerType()),
                        StructField("name", StringType()),
                    ]
                ),
            ]:
                self.assert_eq(
                    self.connect.read.json(path=d, schema=schema).toPandas(),
                    self.spark.read.json(path=d, schema=schema).toPandas(),
                )

            self.assert_eq(
                self.connect.read.json(path=d, primitivesAsString=True).toPandas(),
                self.spark.read.json(path=d, primitivesAsString=True).toPandas(),
            )

    def test_xml(self):
        tmpPath = tempfile.mkdtemp()
        shutil.rmtree(tmpPath)
        xsdPath = tempfile.mkdtemp()
        xsdString = """<?xml version="1.0" encoding="UTF-8" ?>
          <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
            <xs:element name="person">
              <xs:complexType>
                <xs:sequence>
                  <xs:element name="name" type="xs:string" />
                  <xs:element name="age" type="xs:long" />
                </xs:sequence>
              </xs:complexType>
            </xs:element>
          </xs:schema>"""
        try:
            xsdFile = os.path.join(xsdPath, "people.xsd")
            with open(xsdFile, "w") as f:
                _ = f.write(xsdString)
            df = self.spark.createDataFrame([("Hyukjin", 100), ("Aria", 101), ("Arin", 102)]).toDF(
                "name", "age"
            )
            df.write.xml(tmpPath, rootTag="people", rowTag="person")
            people = self.spark.read.xml(tmpPath, rowTag="person", rowValidationXSDPath=xsdFile)
            peopleConnect = self.connect.read.xml(
                tmpPath, rowTag="person", rowValidationXSDPath=xsdFile
            )
            self.assert_eq(people.toPandas(), peopleConnect.toPandas())
            expected = [
                Row(age=100, name="Hyukjin"),
                Row(age=101, name="Aria"),
                Row(age=102, name="Arin"),
            ]
            expectedSchema = StructType(
                [StructField("age", LongType(), True), StructField("name", StringType(), True)]
            )

            self.assertEqual(people.sort("age").collect(), expected)
            self.assertEqual(people.schema, expectedSchema)

            for schema in [
                "age INT, name STRING",
                expectedSchema,
            ]:
                people = self.spark.read.xml(
                    tmpPath, rowTag="person", rowValidationXSDPath=xsdFile, schema=schema
                )
                peopleConnect = self.connect.read.xml(
                    tmpPath, rowTag="person", rowValidationXSDPath=xsdFile, schema=schema
                )
                self.assert_eq(people.toPandas(), peopleConnect.toPandas())

        finally:
            shutil.rmtree(tmpPath)
            shutil.rmtree(xsdPath)

    def test_parquet(self):
        # SPARK-41445: Implement DataFrameReader.parquet
        with tempfile.TemporaryDirectory(prefix="test_parquet") as d:
            # Write a DataFrame into a JSON file
            self.spark.createDataFrame([{"age": 100, "name": "Hyukjin Kwon"}]).write.mode(
                "overwrite"
            ).format("parquet").save(d)
            # Read the Parquet file as a DataFrame.
            self.assert_eq(
                self.connect.read.parquet(d).toPandas(), self.spark.read.parquet(d).toPandas()
            )

    def test_parquet_compression_option(self):
        # SPARK-50537: Fix compression option being overwritten in df.write.parquet
        with tempfile.TemporaryDirectory(prefix="test_parquet") as d:
            self.connect.range(10).write.mode("overwrite").option("compression", "gzip").parquet(d)
            self.assertTrue(any(file.endswith(".gz.parquet") for file in os.listdir(d)))
            # Read the Parquet file as a DataFrame.
            self.assert_eq(
                self.connect.read.parquet(d).toPandas(), self.spark.read.parquet(d).toPandas()
            )

    def test_text(self):
        # SPARK-41849: Implement DataFrameReader.text
        with tempfile.TemporaryDirectory(prefix="test_text") as d:
            # Write a DataFrame into a text file
            self.spark.createDataFrame(
                [{"name": "Sandeep Singh"}, {"name": "Hyukjin Kwon"}]
            ).write.mode("overwrite").format("text").save(d)
            # Read the text file as a DataFrame.
            self.assert_eq(self.connect.read.text(d).toPandas(), self.spark.read.text(d).toPandas())

    def test_csv(self):
        # SPARK-42011: Implement DataFrameReader.csv
        with tempfile.TemporaryDirectory(prefix="test_csv") as d:
            # Write a DataFrame into a text file
            self.spark.createDataFrame(
                [{"name": "Sandeep Singh"}, {"name": "Hyukjin Kwon"}]
            ).write.mode("overwrite").format("csv").save(d)
            # Read the text file as a DataFrame.
            self.assert_eq(self.connect.read.csv(d).toPandas(), self.spark.read.csv(d).toPandas())

    def test_json_with_dataframe_input(self):
        json_df = self.connect.createDataFrame(
            [('{"name": "Alice", "age": 25}',), ('{"name": "Bob", "age": 30}',)],
            schema="value STRING",
        )
        result = self.connect.read.json(json_df)
        expected = [Row(age=25, name="Alice"), Row(age=30, name="Bob")]
        self.assertEqual(sorted(result.collect(), key=lambda r: r.name), expected)

    def test_json_with_dataframe_input_and_schema(self):
        json_df = self.connect.createDataFrame(
            [('{"name": "Alice", "age": 25}',), ('{"name": "Bob", "age": 30}',)],
            schema="value STRING",
        )
        result = self.connect.read.json(json_df, schema="name STRING, age INT")
        expected = [Row(name="Alice", age=25), Row(name="Bob", age=30)]
        self.assertEqual(sorted(result.collect(), key=lambda r: r.name), expected)

    def test_json_with_dataframe_input_non_string_column(self):
        int_df = self.connect.createDataFrame([(1,), (2,)], schema="value INT")
        with self.assertRaisesRegex(Exception, "DATAFRAME_INPUT_NOT_STRING_TYPE"):
            self.connect.read.json(int_df).collect()

    def test_json_with_dataframe_input_multiple_columns(self):
        multi_df = self.connect.createDataFrame(
            [('{"name": "Alice"}', "extra"), ('{"name": "Bob"}', "extra")],
            schema="value STRING, other STRING",
        )
        with self.assertRaisesRegex(Exception, "DATAFRAME_INPUT_NOT_SINGLE_COLUMN"):
            self.connect.read.json(multi_df).collect()

    def test_json_with_dataframe_input_zero_columns(self):
        empty_schema_df = self.connect.range(1).select()
        with self.assertRaisesRegex(Exception, "DATAFRAME_INPUT_NOT_SINGLE_COLUMN"):
            self.connect.read.json(empty_schema_df).collect()

    def test_csv_with_dataframe_input(self):
        csv_df = self.connect.createDataFrame(
            [("Alice,25",), ("Bob,30",)],
            schema="value STRING",
        )
        result = self.connect.read.csv(csv_df)
        expected = [Row(_c0="Alice", _c1="25"), Row(_c0="Bob", _c1="30")]
        self.assertEqual(sorted(result.collect(), key=lambda r: r._c0), expected)

    def test_csv_with_dataframe_input_and_schema(self):
        csv_df = self.connect.createDataFrame(
            [("Alice,25",), ("Bob,30",)],
            schema="value STRING",
        )
        result = self.connect.read.csv(csv_df, schema="name STRING, age INT")
        expected = [Row(name="Alice", age=25), Row(name="Bob", age=30)]
        self.assertEqual(sorted(result.collect(), key=lambda r: r.name), expected)

    def test_csv_with_dataframe_input_non_string_column(self):
        int_df = self.connect.createDataFrame([(1,), (2,)], schema="value INT")
        with self.assertRaisesRegex(Exception, "DATAFRAME_INPUT_NOT_STRING_TYPE"):
            self.connect.read.csv(int_df).collect()

    def test_csv_with_dataframe_input_multiple_columns(self):
        multi_df = self.connect.createDataFrame(
            [("Alice,25", "extra"), ("Bob,30", "extra")],
            schema="value STRING, other STRING",
        )
        with self.assertRaisesRegex(Exception, "DATAFRAME_INPUT_NOT_SINGLE_COLUMN"):
            self.connect.read.csv(multi_df).collect()

    def test_csv_with_dataframe_input_zero_columns(self):
        empty_schema_df = self.connect.range(1).select()
        with self.assertRaisesRegex(Exception, "DATAFRAME_INPUT_NOT_SINGLE_COLUMN"):
            self.connect.read.csv(empty_schema_df).collect()

    def test_xml_with_dataframe_input(self):
        xml_df = self.connect.createDataFrame(
            [
                ("<person><name>Alice</name><age>25</age></person>",),
                ("<person><name>Bob</name><age>30</age></person>",),
            ],
            schema="value STRING",
        )
        result = self.connect.read.option("rowTag", "person").xml(xml_df)
        expected = [Row(age=25, name="Alice"), Row(age=30, name="Bob")]
        self.assertEqual(sorted(result.collect(), key=lambda r: r.name), expected)

    def test_xml_with_dataframe_input_and_schema(self):
        xml_df = self.connect.createDataFrame(
            [
                ("<person><name>Alice</name><age>25</age></person>",),
                ("<person><name>Bob</name><age>30</age></person>",),
            ],
            schema="value STRING",
        )
        result = self.connect.read.option("rowTag", "person").xml(
            xml_df, schema="name STRING, age INT"
        )
        expected = [Row(name="Alice", age=25), Row(name="Bob", age=30)]
        self.assertEqual(sorted(result.collect(), key=lambda r: r.name), expected)

    def test_xml_with_dataframe_input_non_string_column(self):
        int_df = self.connect.createDataFrame([(1,), (2,)], schema="value INT")
        with self.assertRaisesRegex(Exception, "DATAFRAME_INPUT_NOT_STRING_TYPE"):
            self.connect.read.option("rowTag", "person").xml(int_df).collect()

    def test_xml_with_dataframe_input_multiple_columns(self):
        multi_df = self.connect.createDataFrame(
            [
                ("<person><name>Alice</name></person>", "extra"),
                ("<person><name>Bob</name></person>", "extra"),
            ],
            schema="value STRING, other STRING",
        )
        with self.assertRaisesRegex(Exception, "DATAFRAME_INPUT_NOT_SINGLE_COLUMN"):
            self.connect.read.option("rowTag", "person").xml(multi_df).collect()

    def test_xml_with_dataframe_input_zero_columns(self):
        empty_schema_df = self.connect.range(1).select()
        with self.assertRaisesRegex(Exception, "DATAFRAME_INPUT_NOT_SINGLE_COLUMN"):
            self.connect.read.option("rowTag", "person").xml(empty_schema_df).collect()

    def test_multi_paths(self):
        # SPARK-42041: DataFrameReader should support list of paths

        with tempfile.TemporaryDirectory(prefix="test_multi_paths1") as d:
            text_files = []
            for i in range(0, 3):
                text_file = f"{d}/text-{i}.text"
                shutil.copyfile("python/test_support/sql/text-test.txt", text_file)
                text_files.append(text_file)

            self.assertEqual(
                self.connect.read.text(text_files).collect(),
                self.spark.read.text(text_files).collect(),
            )

        with tempfile.TemporaryDirectory(prefix="test_multi_paths2") as d:
            json_files = []
            for i in range(0, 5):
                json_file = f"{d}/json-{i}.json"
                shutil.copyfile("python/test_support/sql/people.json", json_file)
                json_files.append(json_file)

            self.assertEqual(
                self.connect.read.json(json_files).collect(),
                self.spark.read.json(json_files).collect(),
            )

    def test_orc(self):
        # SPARK-42012: Implement DataFrameReader.orc
        with tempfile.TemporaryDirectory(prefix="test_orc") as d:
            # Write a DataFrame into a text file
            self.spark.createDataFrame(
                [{"name": "Sandeep Singh"}, {"name": "Hyukjin Kwon"}]
            ).write.mode("overwrite").format("orc").save(d)
            # Read the text file as a DataFrame.
            self.assert_eq(self.connect.read.orc(d).toPandas(), self.spark.read.orc(d).toPandas())

    def test_simple_datasource_read(self) -> None:
        writeDf = self.df_text
        tmpPath = tempfile.mkdtemp()
        shutil.rmtree(tmpPath)
        writeDf.write.text(tmpPath)

        for schema in [
            "id STRING",
            StructType([StructField("id", StringType())]),
        ]:
            readDf = self.connect.read.format("text").schema(schema).load(path=tmpPath)
            expectResult = writeDf.collect()
            pandasResult = readDf.toPandas()
            if pandasResult is None:
                self.assertTrue(False, "Empty pandas dataframe")
            else:
                actualResult = pandasResult.values.tolist()
                self.assertEqual(len(expectResult), len(actualResult))

    def test_simple_read_without_schema(self) -> None:
        """SPARK-41300: Schema not set when reading CSV."""
        writeDf = self.df_text
        tmpPath = tempfile.mkdtemp()
        shutil.rmtree(tmpPath)
        writeDf.write.csv(tmpPath, header=True)

        readDf = self.connect.read.format("csv").option("header", True).load(path=tmpPath)
        expectResult = set(writeDf.collect())
        pandasResult = set(readDf.collect())
        self.assertEqual(expectResult, pandasResult)

    def test_write_operations(self):
        with tempfile.TemporaryDirectory(prefix="test_write_operations") as d:
            df = self.connect.range(50)
            df.write.mode("overwrite").format("csv").save(d)

            ndf = self.connect.read.schema("id int").load(d, format="csv")
            self.assertEqual(50, len(ndf.collect()))
            cd = ndf.collect()
            self.assertEqual(set(df.collect()), set(cd))

        with tempfile.TemporaryDirectory(prefix="test_write_operations") as d:
            df = self.connect.range(50)
            df.write.mode("overwrite").csv(d, lineSep="|")

            ndf = self.connect.read.schema("id int").load(d, format="csv", lineSep="|")
            self.assertEqual(set(df.collect()), set(ndf.collect()))

        df = self.connect.range(50)
        df.write.format("parquet").saveAsTable("parquet_test")

        ndf = self.connect.read.table("parquet_test")
        self.assertEqual(set(df.collect()), set(ndf.collect()))

    def test_writeTo_operations(self):
        # SPARK-42002: Implement DataFrameWriterV2
        import datetime
        from pyspark.sql.connect.functions import col, years, months, days, hours, bucket

        df = self.connect.createDataFrame(
            [(1, datetime.datetime(2000, 1, 1), "foo")], ("id", "ts", "value")
        )
        writer = df.writeTo("table1")
        self.assertIsInstance(writer.option("property", "value"), DataFrameWriterV2)
        self.assertIsInstance(writer.options(property="value"), DataFrameWriterV2)
        self.assertIsInstance(writer.using("source"), DataFrameWriterV2)
        self.assertIsInstance(writer.partitionedBy(col("id")), DataFrameWriterV2)
        self.assertIsInstance(writer.tableProperty("foo", "bar"), DataFrameWriterV2)
        self.assertIsInstance(writer.partitionedBy(years("ts")), DataFrameWriterV2)
        self.assertIsInstance(writer.partitionedBy(months("ts")), DataFrameWriterV2)
        self.assertIsInstance(writer.partitionedBy(days("ts")), DataFrameWriterV2)
        self.assertIsInstance(writer.partitionedBy(hours("ts")), DataFrameWriterV2)
        self.assertIsInstance(writer.partitionedBy(bucket(11, "id")), DataFrameWriterV2)
        self.assertIsInstance(writer.partitionedBy(bucket(3, "id"), hours("ts")), DataFrameWriterV2)

    def test_simple_udt_from_read(self):
        from pyspark.ml.linalg import Matrices, Vectors

        with tempfile.TemporaryDirectory(prefix="test_simple_udt_from_read") as d:
            path1 = f"{d}/df1.parquet"
            self.spark.createDataFrame(
                [(i % 3, PythonOnlyPoint(float(i), float(i))) for i in range(10)],
                schema=StructType().add("key", LongType()).add("val", PythonOnlyUDT()),
            ).write.parquet(path1)

            path2 = f"{d}/df2.parquet"
            self.spark.createDataFrame(
                [(i % 3, [PythonOnlyPoint(float(i), float(i))]) for i in range(10)],
                schema=StructType().add("key", LongType()).add("val", ArrayType(PythonOnlyUDT())),
            ).write.parquet(path2)

            path3 = f"{d}/df3.parquet"
            self.spark.createDataFrame(
                [(i % 3, {i % 3: PythonOnlyPoint(float(i + 1), float(i + 1))}) for i in range(10)],
                schema=StructType()
                .add("key", LongType())
                .add("val", MapType(LongType(), PythonOnlyUDT())),
            ).write.parquet(path3)

            path4 = f"{d}/df4.parquet"
            self.spark.createDataFrame(
                [(i % 3, PythonOnlyPoint(float(i), float(i))) for i in range(10)],
                schema=StructType().add("key", LongType()).add("val", PythonOnlyUDT()),
            ).write.parquet(path4)

            path5 = f"{d}/df5.parquet"
            self.spark.createDataFrame(
                [Row(label=1.0, point=ExamplePoint(1.0, 2.0))]
            ).write.parquet(path5)

            path6 = f"{d}/df6.parquet"
            self.spark.createDataFrame(
                [(Vectors.dense(1.0, 2.0, 3.0),), (Vectors.sparse(3, {1: 1.0, 2: 5.5}),)],
                ["vec"],
            ).write.parquet(path6)

            path7 = f"{d}/df7.parquet"
            self.spark.createDataFrame(
                [
                    (Matrices.dense(3, 2, [0, 1, 4, 5, 9, 10]),),
                    (Matrices.sparse(1, 1, [0, 1], [0], [2.0]),),
                ],
                ["mat"],
            ).write.parquet(path7)

            for path in [path1, path2, path3, path4, path5, path6, path7]:
                self.assertEqual(
                    self.connect.read.parquet(path).schema,
                    self.spark.read.parquet(path).schema,
                )

    # DataStreamReader.name() tests - require source evolution configs
    def test_stream_reader_name_valid_names(self):
        """Test that various valid source name patterns work correctly."""
        with self.connect_conf(
            {
                "spark.sql.streaming.queryEvolution.enableSourceEvolution": "true",
                "spark.sql.streaming.offsetLog.formatVersion": "2",
            }
        ):
            valid_names = [
                "mySource",
                "my_source",
                "MySource123",
                "_private",
                "source_123_test",
                "123source",
            ]

            for name in valid_names:
                with tempfile.TemporaryDirectory(prefix=f"test_{name}_") as tmpdir:
                    self.connect.range(10).write.mode("overwrite").parquet(tmpdir)
                    df = (
                        self.connect.readStream.format("parquet")
                        .schema("id LONG")
                        .name(name)
                        .load(tmpdir)
                    )
                    self.assertTrue(
                        df.isStreaming, f"DataFrame should be streaming for name: {name}"
                    )

    def test_stream_reader_name_method_chaining(self):
        """Test that name() returns the reader for method chaining."""
        with self.connect_conf(
            {
                "spark.sql.streaming.queryEvolution.enableSourceEvolution": "true",
                "spark.sql.streaming.offsetLog.formatVersion": "2",
            }
        ):
            with tempfile.TemporaryDirectory(prefix="test_chaining_") as tmpdir:
                self.connect.range(10).write.mode("overwrite").parquet(tmpdir)
                df = (
                    self.connect.readStream.format("parquet")
                    .schema("id LONG")
                    .name("my_source")
                    .option("maxFilesPerTrigger", "1")
                    .load(tmpdir)
                )

                self.assertTrue(df.isStreaming, "DataFrame should be streaming")

    def test_stream_reader_name_before_format(self):
        """Test that order doesn't matter - name can be set before format."""
        with self.connect_conf(
            {
                "spark.sql.streaming.queryEvolution.enableSourceEvolution": "true",
                "spark.sql.streaming.offsetLog.formatVersion": "2",
            }
        ):
            with tempfile.TemporaryDirectory(prefix="test_before_format_") as tmpdir:
                self.connect.range(10).write.mode("overwrite").parquet(tmpdir)
                df = (
                    self.connect.readStream.name("my_source")
                    .format("parquet")
                    .schema("id LONG")
                    .load(tmpdir)
                )

                self.assertTrue(df.isStreaming, "DataFrame should be streaming")

    def test_stream_reader_invalid_names(self):
        """Test that various invalid source names are rejected."""
        with self.connect_conf(
            {
                "spark.sql.streaming.queryEvolution.enableSourceEvolution": "true",
                "spark.sql.streaming.offsetLog.formatVersion": "2",
            }
        ):
            invalid_names = [
                "",  # empty string
                "  ",  # whitespace only
                "my-source",  # hyphen
                "my source",  # space
                "my.source",  # dot
                "my@source",  # special char
                "my$source",  # dollar sign
                "my#source",  # hash
                "my!source",  # exclamation
            ]

            for invalid_name in invalid_names:
                with self.subTest(name=invalid_name):
                    with tempfile.TemporaryDirectory(prefix="test_invalid_") as tmpdir:
                        self.connect.range(10).write.mode("overwrite").parquet(tmpdir)
                        with self.assertRaises(PySparkValueError) as context:
                            self.connect.readStream.format("parquet").schema("id LONG").name(
                                invalid_name
                            ).load(tmpdir)

                        # The error message should contain information about invalid name
                        self.assertIn("source", str(context.exception).lower())

    def test_stream_reader_invalid_name_wrong_type(self):
        """Test that None and non-string types are rejected."""
        invalid_types = [None, 123, 45.67, [], {}]

        for invalid_value in invalid_types:
            with self.subTest(value=invalid_value):
                with self.assertRaises(PySparkTypeError):
                    self.connect.readStream.format("rate").name(invalid_value).load()

    def test_stream_reader_name_with_different_formats(self):
        """Test that name() works with different streaming data sources."""
        with self.connect_conf(
            {
                "spark.sql.streaming.queryEvolution.enableSourceEvolution": "true",
                "spark.sql.streaming.offsetLog.formatVersion": "2",
            }
        ):
            with tempfile.TemporaryDirectory(prefix="test_name_formats_") as tmpdir:
                # Create test data
                self.connect.range(10).write.mode("overwrite").parquet(tmpdir + "/parquet_data")
                self.connect.range(10).selectExpr("id", "CAST(id AS STRING) as value").write.mode(
                    "overwrite"
                ).json(tmpdir + "/json_data")

                # Test with parquet
                parquet_df = (
                    self.connect.readStream.format("parquet")
                    .name("parquet_source")
                    .schema("id LONG")
                    .load(tmpdir + "/parquet_data")
                )
                self.assertTrue(parquet_df.isStreaming, "Parquet DataFrame should be streaming")

                # Test with json - specify schema
                json_df = (
                    self.connect.readStream.format("json")
                    .name("json_source")
                    .schema("id LONG, value STRING")
                    .load(tmpdir + "/json_data")
                )
                self.assertTrue(json_df.isStreaming, "JSON DataFrame should be streaming")

    def test_stream_reader_name_persists_through_query(self):
        """Test that the name persists when starting a streaming query."""
        with self.connect_conf(
            {
                "spark.sql.streaming.queryEvolution.enableSourceEvolution": "true",
                "spark.sql.streaming.offsetLog.formatVersion": "2",
            }
        ):
            with tempfile.TemporaryDirectory(prefix="test_name_query_") as tmpdir:
                data_dir = tmpdir + "/data"
                checkpoint_dir = tmpdir + "/checkpoint"

                # Create test data
                self.connect.range(10).write.mode("overwrite").parquet(data_dir)

                df = (
                    self.connect.readStream.format("parquet")
                    .schema("id LONG")
                    .name("parquet_source_test")
                    .load(data_dir)
                )

                query = (
                    df.writeStream.format("noop")
                    .option("checkpointLocation", checkpoint_dir)
                    .start()
                )

                try:
                    # Let it run briefly
                    time.sleep(1)

                    # Verify query is running
                    self.assertTrue(query.isActive, "Query should be active")
                finally:
                    query.stop()


if __name__ == "__main__":
    from pyspark.testing import main

    main()
