import io
import os
import unittest
import uuid

import tos

from tosfsspec import TosFileSystem


class TosFileTestMixin:
    bucket_type = 'fns'

    @classmethod
    def setUpClass(cls):
        cls.region = os.environ.get('TOS_REGION')
        cls.endpoint = os.environ.get('TOS_ENDPOINT', '')
        cls.access_key = os.environ.get('TOS_ACCESS_KEY', '')
        cls.secret_key = os.environ.get('TOS_SECRET_KEY', '')

        if not all([cls.region, cls.access_key, cls.secret_key]):
            raise unittest.SkipTest("TOS environment variables (REGION, ACCESS_KEY, SECRET_KEY) not set")

        cls.fs = TosFileSystem(
            region=cls.region,
            endpoint=cls.endpoint,
            key=cls.access_key,
            secret=cls.secret_key
        )
        cls.bucket_name = f"test-tos-file-{cls.bucket_type}-{uuid.uuid4().hex}"
        try:
            cls.fs.mkdir(cls.bucket_name, create_bucket=True, bucket_type=cls.bucket_type)
        except Exception as e:
            print(f"Failed to create bucket {cls.bucket_name}: {e}")
            raise

        # Initialize native tos client for cleanup operations not supported by fs
        cls.tos_client = tos.TosClientV2(
            ak=cls.access_key,
            sk=cls.secret_key,
            endpoint=cls.endpoint,
            region=cls.region
        )

    @classmethod
    def tearDownClass(cls):
        try:
            # Clean up bucket contents including multipart uploads
            if cls.bucket_type == 'hns':
                cls._cleanup_hns()
            else:
                cls._cleanup_fns()

            # Delete the bucket
            cls.fs.rmdir(cls.bucket_name, delete_bucket=True)
        except Exception as e:
            print(f"Failed to delete bucket {cls.bucket_name}: {e}")
            raise

    @classmethod
    def _cleanup_fns(cls):
        try:
            # 1. Abort all multipart uploads
            upload_id_marker = None
            while True:
                output = cls.tos_client.list_multipart_uploads(cls.bucket_name, upload_id_marker=upload_id_marker)
                if output.uploads:
                    for upload in output.uploads:
                        cls.tos_client.abort_multipart_upload(cls.bucket_name, upload.key, upload.upload_id)

                if not output.is_truncated:
                    break
                upload_id_marker = output.next_upload_id_marker

            # 2. Delete all objects (files and directories)
            files = cls.fs.ls(cls.bucket_name, detail=False)
            for f in files:
                cls.fs.rm(f, recursive=True)

        except Exception as e:
            print(f"Error cleaning up FNS bucket contents: {e}")
            raise

    @classmethod
    def _cleanup_hns(cls):
        try:
            # 1. Abort all multipart uploads
            upload_id_marker = None
            while True:
                output = cls.tos_client.list_multipart_uploads(cls.bucket_name, upload_id_marker=upload_id_marker)
                if output.uploads:
                    for upload in output.uploads:
                        cls.tos_client.abort_multipart_upload(cls.bucket_name, upload.key, upload.upload_id)

                if not output.is_truncated:
                    break
                upload_id_marker = output.next_upload_id_marker

            # 2. Recursive delete using TOS SDK directly
            def delete_recursive(prefix=''):
                marker = ''
                while True:
                    output = cls.tos_client.list_objects(cls.bucket_name, prefix=prefix, delimiter='/', marker=marker)

                    # Delete files
                    if output.contents:
                        for obj in output.contents:
                            if obj.key.endswith('/'):
                                continue
                            cls.tos_client.delete_object(cls.bucket_name, obj.key)

                    # Process subdirectories
                    if output.common_prefixes:
                        for cp in output.common_prefixes:
                            # cp.prefix contains the subdirectory path ending with '/'
                            delete_recursive(cp.prefix)
                            # Delete the empty directory itself
                            cls.tos_client.delete_object(cls.bucket_name, cp.prefix)

                    if not output.is_truncated:
                        break
                    marker = output.next_marker

            delete_recursive()

        except Exception as e:
            print(f"Error cleaning up HNS bucket contents: {e}")
            raise

    def setUp(self):
        self.test_dir = f"{self.bucket_name}/test-{uuid.uuid4().hex}"
        self.fs.makedirs(self.test_dir)

    def tearDown(self):
        try:
            self.fs.rm(self.test_dir, recursive=True)
        except Exception:
            pass

    """
       Detailed tests for TosFile implementation focusing on read/write/seek operations
       """

    def test_read_all(self):
        path = f"{self.test_dir}/read_all.txt"
        data = b"Hello world! This is a test."
        with self.fs.open(path, "wb") as f:
            f.write(data)

        with self.fs.open(path, "rb") as f:
            read_data = f.read()
            self.assertEqual(read_data, data)
            self.assertEqual(f.tell(), len(data))

    def test_read_chunked(self):
        path = f"{self.test_dir}/read_chunked.txt"
        data = b"0123456789" * 100  # 1000 bytes
        with self.fs.open(path, "wb") as f:
            f.write(data)

        with self.fs.open(path, "rb") as f:
            chunk1 = f.read(100)
            self.assertEqual(chunk1, data[:100])
            self.assertEqual(f.tell(), 100)

            chunk2 = f.read(200)
            self.assertEqual(chunk2, data[100:300])
            self.assertEqual(f.tell(), 300)

            rest = f.read()
            self.assertEqual(rest, data[300:])
            self.assertEqual(f.tell(), 1000)

    def test_readinto(self):
        path = f"{self.test_dir}/readinto.txt"
        data = b"abcdefghij"
        with self.fs.open(path, "wb") as f:
            f.write(data)

        with self.fs.open(path, "rb") as f:
            buf = bytearray(5)
            n = f.readinto(buf)
            self.assertEqual(n, 5)
            self.assertEqual(buf, b"abcde")
            self.assertEqual(f.tell(), 5)

            buf = bytearray(5)
            n = f.readinto(buf)
            self.assertEqual(n, 5)
            self.assertEqual(buf, b"fghij")

            # Read past EOF
            n = f.readinto(buf)
            self.assertEqual(n, 0)

    def test_readline(self):
        path = f"{self.test_dir}/readline.txt"
        lines = [b"line1\n", b"line2\n", b"line3"]
        data = b"".join(lines)
        with self.fs.open(path, "wb") as f:
            f.write(data)

        with self.fs.open(path, "rb") as f:
            self.assertEqual(f.readline(), lines[0])
            self.assertEqual(f.readline(), lines[1])
            self.assertEqual(f.readline(), lines[2])
            self.assertEqual(f.readline(), b"")

    def test_seek_tell(self):
        path = f"{self.test_dir}/seek.txt"
        data = b"0123456789"
        with self.fs.open(path, "wb") as f:
            f.write(data)

        with self.fs.open(path, "rb") as f:
            # Start
            self.assertEqual(f.tell(), 0)

            # Seek from start
            pos = f.seek(5)
            self.assertEqual(pos, 5)
            self.assertEqual(f.read(1), b"5")

            # Seek from current
            pos = f.seek(2, io.SEEK_CUR)  # 6 + 2 = 8
            self.assertEqual(pos, 8)
            self.assertEqual(f.read(1), b"8")

            # Seek from end
            pos = f.seek(-2, io.SEEK_END)  # 10 - 2 = 8
            self.assertEqual(pos, 8)
            self.assertEqual(f.read(1), b"8")

            # Seek to start
            pos = f.seek(0)
            self.assertEqual(pos, 0)
            self.assertEqual(f.read(1), b"0")

    def test_seek_invalid(self):
        path = f"{self.test_dir}/seek_invalid.txt"
        self.fs.touch(path)

        with self.fs.open(path, "rb") as f:
            with self.assertRaises(ValueError):
                f.seek(-1)

            with self.assertRaises(ValueError):
                f.seek(-10, io.SEEK_CUR)

    def test_write_types(self):
        path = f"{self.test_dir}/write_types.txt"

        # Write bytes
        with self.fs.open(path, "wb") as f:
            f.write(b"bytes")
        self.assertEqual(self.fs.cat(path), b"bytes")

        # Write string
        with self.fs.open(path, "wb") as f:
            f.write("string")
        self.assertEqual(self.fs.cat(path), b"string")

        # Write memoryview
        with self.fs.open(path, "wb") as f:
            f.write(memoryview(b"memoryview"))
        self.assertEqual(self.fs.cat(path), b"memoryview")

    def test_flush_closed(self):
        path = f"{self.test_dir}/flush.txt"
        with self.fs.open(path, "wb") as f:
            f.write(b"data")
            f.flush()
            # Should not raise error

        with self.assertRaises(ValueError):
            f.write(b"more")

        with self.assertRaises(ValueError):
            f.read()

    def test_context_manager(self):
        path = f"{self.test_dir}/context.txt"
        f = self.fs.open(path, "wb")
        with f:
            f.write(b"content")

        self.assertTrue(f.closed)
        self.assertTrue(self.fs.exists(path))

    def test_large_io(self):
        # Test file larger than default chunk size (typically 4MB or 8MB)
        # Using 9MB to be safe
        size = 9 * 1024 * 1024
        path = f"{self.test_dir}/large.bin"
        # Create dummy data efficiently?
        # Creating 9MB in memory is fine.
        data = b"x" * size

        with self.fs.open(path, "wb") as f:
            f.write(data)

        info = self.fs.info(path)
        self.assertEqual(info['size'], size)

        with self.fs.open(path, "rb") as f:
            # Read first chunk
            chunk = f.read(1024)
            self.assertEqual(chunk, b"x" * 1024)

            # Seek to near end
            f.seek(-1024, io.SEEK_END)
            chunk = f.read()
            self.assertEqual(len(chunk), 1024)
            self.assertEqual(chunk, b"x" * 1024)

    def test_read_past_eof(self):
        path = f"{self.test_dir}/eof.txt"
        self.fs.touch(path)
        with self.fs.open(path, "rb") as f:
            self.assertEqual(f.read(), b"")
            self.assertEqual(f.read(10), b"")

        with self.fs.open(path, "wb") as f:
            f.write(b"abc")

        with self.fs.open(path, "rb") as f:
            f.seek(100)
            self.assertEqual(f.read(), b"")


class TosFileTestFNS(TosFileTestMixin, unittest.TestCase):
    bucket_name = "fns"


class TosFileTestHNS(TosFileTestMixin, unittest.TestCase):
    bucket_name = "hns"


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