# 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 pyarrow as pa

from pypaimon import PaimonVirtualFileSystem, Schema
from pypaimon.table.object import ObjectTable
from pypaimon.tests.rest.rest_base_test import RESTBaseTest


class RESTObjectTableTest(RESTBaseTest):

    def setUp(self):
        super().setUp()
        pvfs_options = {
            'uri': self.options['uri'],
            'warehouse': self.options['warehouse'],
            'dlf.region': self.options['dlf.region'],
            'token.provider': self.options['token.provider'],
            'token': self.options['token'],
        }
        self.pvfs = PaimonVirtualFileSystem(pvfs_options)

    def _create_object_table(self, table_name, extra_options=None):
        """Helper to create an object table with the given name.

        ObjectTable has a fixed schema and does not support custom fields.
        Only options (including type=object-table) are needed.
        """
        options = {"type": "object-table"}
        if extra_options:
            options.update(extra_options)
        schema = Schema(options=options)
        self.rest_catalog.drop_table(table_name, True)
        self.rest_catalog.create_table(table_name, schema, False)
        return self.rest_catalog.get_table(table_name)

    def _pvfs_table_path(self, table_name, sub_path=None):
        """Build a pvfs:// path for the given table and optional sub-path."""
        warehouse = self.options['warehouse']
        base = "pvfs://{}/{}".format(warehouse, table_name.replace('.', '/'))
        if sub_path:
            return "{}/{}".format(base, sub_path)
        return base

    def _write_file_via_pvfs(self, table_name, filename, content):
        """Write a file into the table's location using pvfs."""
        path = self._pvfs_table_path(table_name, filename)
        # Ensure parent directory exists when filename contains subdirectories
        if '/' in filename:
            parent_dir = self._pvfs_table_path(
                table_name, filename.rsplit('/', 1)[0]
            )
            self.pvfs.makedirs(parent_dir, exist_ok=True)
        with self.pvfs.open(path, 'wb') as f:
            f.write(content)

    def test_get_object_table(self):
        table_name = "default.object_table_basic"
        table = self._create_object_table(table_name)

        self.assertIsInstance(table, ObjectTable)
        self.assertEqual(table.name(), "object_table_basic")
        self.assertEqual(table.full_name(), table_name)
        self.assertEqual(table.partition_keys, [])
        self.assertEqual(table.primary_keys, [])
        self.assertEqual(table.options().get("type"), "object-table")

    def test_object_table_read_files(self):
        table_name = "default.object_table_read"
        table = self._create_object_table(table_name)

        # Write some test files into the table's location via pvfs
        test_files = {
            "file_a.txt": b"hello world",
            "file_b.dat": b"some binary data here",
        }
        for filename, content in test_files.items():
            self._write_file_via_pvfs(table_name, filename, content)

        # Read the object table
        read_builder = table.new_read_builder()
        scan = read_builder.new_scan()
        plan = scan.plan()
        splits = plan.splits()

        self.assertEqual(len(splits), 1)

        table_read = read_builder.new_read()
        result = table_read.to_arrow(splits)

        self.assertIsInstance(result, pa.Table)
        # Should contain the schema, snapshot and manifest files plus our test files
        # Filter to only our test files
        result_names = result.column("name").to_pylist()
        for filename in test_files:
            self.assertIn(filename, result_names)

        # Verify file sizes for our test files
        result_dict = {}
        for i in range(result.num_rows):
            name = result.column("name")[i].as_py()
            length = result.column("length")[i].as_py()
            result_dict[name] = length

        for filename, content in test_files.items():
            self.assertEqual(result_dict[filename], len(content))

        # Verify schema columns
        self.assertIn("path", result.column_names)
        self.assertIn("name", result.column_names)
        self.assertIn("length", result.column_names)
        self.assertIn("mtime", result.column_names)
        self.assertIn("atime", result.column_names)
        self.assertIn("owner", result.column_names)

    def test_object_table_read_with_subdirectories(self):
        table_name = "default.object_table_subdir"
        table = self._create_object_table(table_name)

        # Write files via pvfs, including a nested subdirectory file
        self._write_file_via_pvfs(table_name, "root_file.txt", b"root content")
        self._write_file_via_pvfs(table_name, "subdir/nested_file.txt", b"nested content")

        read_builder = table.new_read_builder()
        splits = read_builder.new_scan().plan().splits()
        result = read_builder.new_read().to_arrow(splits)

        result_names = result.column("name").to_pylist()
        self.assertIn("root_file.txt", result_names)
        self.assertIn("nested_file.txt", result_names)

        # Verify relative paths contain subdirectory
        result_paths = result.column("path").to_pylist()
        nested_paths = [p for p in result_paths if "nested_file.txt" in p]
        self.assertTrue(len(nested_paths) > 0)
        self.assertIn("subdir/", nested_paths[0])

    def test_object_table_with_projection(self):
        table_name = "default.object_table_projection"
        table = self._create_object_table(table_name)

        self._write_file_via_pvfs(table_name, "proj_test.txt", b"test data")

        # Test projection with two columns
        read_builder = table.new_read_builder()
        read_builder.with_projection(["name", "length"])
        splits = read_builder.new_scan().plan().splits()
        result = read_builder.new_read().to_arrow(splits)

        self.assertEqual(result.column_names, ["name", "length"])
        self.assertNotIn("path", result.column_names)
        self.assertNotIn("mtime", result.column_names)
        self.assertNotIn("atime", result.column_names)
        self.assertNotIn("owner", result.column_names)

        # Verify data content for our test file
        result_names = result.column("name").to_pylist()
        self.assertIn("proj_test.txt", result_names)
        idx = result_names.index("proj_test.txt")
        self.assertEqual(result.column("length")[idx].as_py(), len(b"test data"))

    def test_object_table_with_limit(self):
        table_name = "default.object_table_limit"
        table = self._create_object_table(table_name)

        for i in range(5):
            self._write_file_via_pvfs(
                table_name, "file_{}.txt".format(i), "content {}".format(i).encode()
            )

        read_builder = table.new_read_builder()
        read_builder.with_limit(2)
        splits = read_builder.new_scan().plan().splits()
        result = read_builder.new_read().to_arrow(splits)

        self.assertEqual(result.num_rows, 2)

    def test_object_table_options_and_copy(self):
        table_name = "default.object_table_options"
        table = self._create_object_table(
            table_name, extra_options={"custom.key": "custom_value"}
        )

        self.assertEqual(table.options().get("custom.key"), "custom_value")
        self.assertEqual(table.options().get("type"), "object-table")

        # Test copy with dynamic options
        copied = table.copy({"new.key": "new_value"})
        self.assertIsInstance(copied, ObjectTable)
        self.assertEqual(copied.options().get("custom.key"), "custom_value")
        self.assertEqual(copied.options().get("new.key"), "new_value")

        # Original should not be modified
        self.assertIsNone(table.options().get("new.key"))

        # Test copy with override
        overridden = table.copy({"custom.key": "overridden"})
        self.assertEqual(overridden.options().get("custom.key"), "overridden")
        self.assertEqual(table.options().get("custom.key"), "custom_value")

    def test_object_table_unsupported_write(self):
        table_name = "default.object_table_no_write"
        table = self._create_object_table(table_name)

        with self.assertRaises(NotImplementedError):
            table.new_batch_write_builder()
        with self.assertRaises(NotImplementedError):
            table.new_stream_write_builder()

    def test_object_table_unsupported_drop_partitions(self):
        table_name = "default.object_table_no_drop_partitions"
        self._create_object_table(table_name)

        with self.assertRaisesRegex(
            ValueError,
            "drop_partitions is not supported for table type 'ObjectTable'",
        ):
            self.rest_catalog.drop_partitions(
                table_name,
                [{"dt": "20250101"}],
            )

    def test_object_table_to_pandas(self):
        table_name = "default.object_table_pandas"
        table = self._create_object_table(table_name)

        self._write_file_via_pvfs(table_name, "pandas_test.txt", b"pandas data")

        read_builder = table.new_read_builder()
        splits = read_builder.new_scan().plan().splits()
        result_df = read_builder.new_read().to_pandas(splits)

        self.assertIn("name", result_df.columns)
        self.assertIn("pandas_test.txt", result_df["name"].values)
