import pytest
import asyncio
from typing import AsyncGenerator, Optional
from nexios import get_application, NexiosApp
from nexios.http import Request, Response
from nexios.structs import FormData, Headers, UploadedFile
from nexios.http.formparsers import FormParser, MultiPartParser, MultiPartException
from nexios.testing import Client

# Set default limits for MultiPartParser if they don't exist
if not hasattr(MultiPartParser, "max_file_size"):
    MultiPartParser.max_file_size = 1024 * 1024  # 1MB
if not hasattr(MultiPartParser, "max_fields"):
    MultiPartParser.max_fields = 1000
if not hasattr(MultiPartParser, "max_files"):
    MultiPartParser.max_files = 1000

# Create an application instance for testing
app = get_application()


# Define test endpoints for form submission
@app.post("/form")
async def handle_form(request: Request, response: Response):
    form = await request.form_data
    return response.json({"fields": dict(form.items())})


@app.post("/upload")
async def handle_upload(request: Request, response: Response):
    form = await request.form_data
    files = {}
    fields = {}

    for key, value in form.items():
        if isinstance(value, UploadedFile):
            content = await value.read()
            files[key] = {
                "filename": value.filename,
                "content": content.decode("utf-8", errors="replace"),
                "size": value.size,
                "content_type": value.headers.get("content-type", ""),
            }
        else:
            fields[key] = value

    return response.json({"files": files, "fields": fields})


# Helper function to create a stream from bytes for testing parsers
async def create_form_stream(data: bytes) -> AsyncGenerator[bytes, None]:
    yield data


# Pytest fixture for client
@pytest.fixture
async def client():
    async with Client(app) as client:
        yield client


# Tests for basic form data parsing
async def test_basic_form_parsing():
    # Create simple form data
    form_data = b"name=John&age=30&email=john%40example.com"
    headers = Headers(
        [
            (b"content-type", b"application/x-www-form-urlencoded"),
            (b"content-length", str(len(form_data)).encode()),
        ]
    )

    # Parse the form data
    parser = FormParser(headers, create_form_stream(form_data))
    form = await parser.parse()

    # Verify parsed data
    assert isinstance(form, FormData)
    assert dict(form.items()) == {
        "name": "John",
        "age": "30",
        "email": "john@example.com",
    }


async def test_empty_form():
    form_data = b""
    headers = Headers(
        [
            (b"content-type", b"application/x-www-form-urlencoded"),
            (b"content-length", b"0"),
        ]
    )

    parser = FormParser(headers, create_form_stream(form_data))
    form = await parser.parse()

    assert isinstance(form, FormData)
    assert len(form) == 0


async def test_form_with_special_characters():
    # Test URL-encoded special characters
    form_data = b"message=Hello+World%21&symbols=%24%26%2B%2C%3A%3B%3D%3F%40"
    headers = Headers(
        [
            (b"content-type", b"application/x-www-form-urlencoded"),
            (b"content-length", str(len(form_data)).encode()),
        ]
    )

    parser = FormParser(headers, create_form_stream(form_data))
    form = await parser.parse()

    assert dict(form.items()) == {"message": "Hello World!", "symbols": "$&+,:;=?@"}


# Tests for multipart form data
async def test_multipart_text_fields():
    boundary = b"boundary123"
    form_data = (
        b"--" + boundary + b"\r\n"
        b'Content-Disposition: form-data; name="field1"\r\n\r\n'
        b"value1\r\n"
        b"--" + boundary + b"\r\n"
        b'Content-Disposition: form-data; name="field2"\r\n\r\n'
        b"value2\r\n"
        b"--" + boundary + b"--\r\n"
    )

    headers = Headers(
        [
            (b"content-type", b"multipart/form-data; boundary=" + boundary),
            (b"content-length", str(len(form_data)).encode()),
        ]
    )

    parser = MultiPartParser(headers, create_form_stream(form_data))
    form = await parser.parse()

    assert dict(form.items()) == {"field1": "value1", "field2": "value2"}


async def test_multipart_file_upload():
    boundary = b"boundary123"
    file_content = b"Hello, this is a test file content!"
    form_data = (
        b"--" + boundary + b"\r\n"
        b'Content-Disposition: form-data; name="file1"; filename="test.txt"\r\n'
        b"Content-Type: text/plain\r\n\r\n" + file_content + b"\r\n"
        b"--" + boundary + b"--\r\n"
    )

    headers = Headers(
        [
            (b"content-type", b"multipart/form-data; boundary=" + boundary),
            (b"content-length", str(len(form_data)).encode()),
        ]
    )

    parser = MultiPartParser(headers, create_form_stream(form_data))
    form = await parser.parse()

    assert len(form) == 1
    file = form.get("file1")
    assert isinstance(file, UploadedFile)
    assert file.filename == "test.txt"
    content = await file.read()
    assert content == file_content


async def test_multipart_mixed_content():
    boundary = b"boundary123"
    file_content = b"Test file content"
    form_data = (
        b"--" + boundary + b"\r\n"
        b'Content-Disposition: form-data; name="field1"\r\n\r\n'
        b"text_value\r\n"
        b"--" + boundary + b"\r\n"
        b'Content-Disposition: form-data; name="file1"; filename="test.txt"\r\n'
        b"Content-Type: text/plain\r\n\r\n" + file_content + b"\r\n"
        b"--" + boundary + b"--\r\n"
    )

    headers = Headers(
        [
            (b"content-type", b"multipart/form-data; boundary=" + boundary),
            (b"content-length", str(len(form_data)).encode()),
        ]
    )

    parser = MultiPartParser(headers, create_form_stream(form_data))
    form = await parser.parse()

    assert len(form) == 2
    assert form.get("field1") == "text_value"
    file = form.get("file1")
    assert isinstance(file, UploadedFile)
    content = await file.read()
    assert content == file_content


async def test_multipart_multiple_files():
    boundary = b"boundary123"
    file1_content = b"Content of file 1"
    file2_content = b"Content of file 2"
    form_data = (
        b"--" + boundary + b"\r\n"
        b'Content-Disposition: form-data; name="file1"; filename="file1.txt"\r\n'
        b"Content-Type: text/plain\r\n\r\n" + file1_content + b"\r\n"
        b"--" + boundary + b"\r\n"
        b'Content-Disposition: form-data; name="file2"; filename="file2.txt"\r\n'
        b"Content-Type: text/plain\r\n\r\n" + file2_content + b"\r\n"
        b"--" + boundary + b"--\r\n"
    )

    headers = Headers(
        [
            (b"content-type", b"multipart/form-data; boundary=" + boundary),
            (b"content-length", str(len(form_data)).encode()),
        ]
    )

    parser = MultiPartParser(headers, create_form_stream(form_data))
    form = await parser.parse()

    assert len(form) == 2
    file1 = form.get("file1")
    file2 = form.get("file2")

    assert isinstance(file1, UploadedFile)
    assert isinstance(file2, UploadedFile)

    assert file1.filename == "file1.txt"
    assert file2.filename == "file2.txt"

    content1 = await file1.read()
    content2 = await file2.read()

    assert content1 == file1_content
    assert content2 == file2_content


# Edge cases tests
async def test_max_file_size_limit():
    boundary = b"boundary123"
    # Make file content slightly larger than the max file size
    # This assumes MultiPartParser.max_file_size is accessible and not too large for testing
    file_size = 1024 * 10  # Use a smaller size for testing
    original_max_size = MultiPartParser.max_file_size
    MultiPartParser.max_file_size = file_size

    try:
        file_content = b"x" * (file_size + 100)  # Exceed max file size
        form_data = (
            b"--" + boundary + b"\r\n"
            b'Content-Disposition: form-data; name="file"; filename="large.txt"\r\n'
            b"Content-Type: text/plain\r\n\r\n" + file_content + b"\r\n"
            b"--" + boundary + b"--\r\n"
        )

        headers = Headers(
            [
                (b"content-type", b"multipart/form-data; boundary=" + boundary),
                (b"content-length", str(len(form_data)).encode()),
            ]
        )

        parser = MultiPartParser(headers, create_form_stream(form_data))

        # This should raise an exception due to file size
        with pytest.raises(MultiPartException):
            await parser.parse()
    finally:
        # Restore original max file size
        MultiPartParser.max_file_size = original_max_size


async def test_max_field_count_limit():
    # Test with more fields than max_fields
    max_fields = 5
    # Store original value
    original_max_fields = MultiPartParser.max_fields
    # Temporarily set max_fields to test value
    MultiPartParser.max_fields = max_fields

    try:
        # Create a multipart form with more than max_fields
        boundary = b"boundary123"
        parts = []
        for i in range(max_fields + 2):
            parts.append(
                b"--"
                + boundary
                + b"\r\n"
                + f'Content-Disposition: form-data; name="field{i}"\r\n\r\n'.encode()
                + f"value{i}\r\n".encode()
            )
        parts.append(b"--" + boundary + b"--\r\n")

        form_data = b"".join(parts)

        headers = Headers(
            [
                (b"content-type", b"multipart/form-data; boundary=" + boundary),
                (b"content-length", str(len(form_data)).encode()),
            ]
        )

        parser = MultiPartParser(
            headers, create_form_stream(form_data), max_fields=max_fields
        )

        # This should raise an exception due to too many fields
        with pytest.raises(MultiPartException):
            await parser.parse()
    finally:
        # Restore original max fields
        MultiPartParser.max_fields = original_max_fields


async def test_character_encoding_handling():
    # Test with non-ASCII characters
    boundary = b"boundary123"
    unicode_text = "Hello 世界 こんにちは"  # Unicode text
    form_data = (
        b"--" + boundary + b"\r\n"
        b'Content-Disposition: form-data; name="text"\r\n'
        b"Content-Type: text/plain; charset=utf-8\r\n\r\n"
        + unicode_text.encode("utf-8")
        + b"\r\n"
        b"--" + boundary + b"--\r\n"
    )

    headers = Headers(
        [
            (
                b"content-type",
                b"multipart/form-data; boundary=" + boundary + b"; charset=utf-8",
            ),
            (b"content-length", str(len(form_data)).encode()),
        ]
    )

    parser = MultiPartParser(headers, create_form_stream(form_data))
    form = await parser.parse()

    assert form.get("text") == unicode_text


# Error handling tests
async def test_malformed_headers():
    # Test with malformed content-disposition header
    boundary = b"boundary123"
    form_data = (
        b"--" + boundary + b"\r\n"
        b"Invalid-Header: value\r\n\r\n"  # Missing Content-Disposition
        b"some content\r\n"
        b"--" + boundary + b"--\r\n"
    )

    headers = Headers(
        [
            (b"content-type", b"multipart/form-data; boundary=" + boundary),
            (b"content-length", str(len(form_data)).encode()),
        ]
    )

    parser = MultiPartParser(headers, create_form_stream(form_data))

    # Should raise an exception due to missing Content-Disposition header
    with pytest.raises(Exception):
        await parser.parse()


async def test_invalid_boundary():
    # Test with incorrect boundary in form data
    boundary = b"correct_boundary"
    wrong_boundary = b"wrong_boundary"

    form_data = (
        b"--" + wrong_boundary + b"\r\n"  # Using wrong boundary
        b'Content-Disposition: form-data; name="field1"\r\n\r\n'
        b"value1\r\n"
        b"--" + wrong_boundary + b"--\r\n"
    )

    headers = Headers(
        raw=[
            (
                b"content-type",
                b"multipart/form-data; boundary=" + boundary,
            ),  # Different boundary
            (b"content-length", str(len(form_data)).encode()),
        ]
    )

    parser = MultiPartParser(headers, create_form_stream(form_data))

    # Should fail to parse properly
    with pytest.raises(Exception):
        await parser.parse()


# Integration tests with client
async def test_simple_form_submission(client):
    # Test basic form submission
    data = {
        "name": "John Doe",
        "email": "john@example.com",
        "message": "This is a test message",
    }

    response = await client.post("/form", data=data)
    assert response.status_code == 200

    result = response.json()
    assert result["fields"] == data


async def test_form_with_special_chars_integration(client):
    # Test form with special characters
    data = {
        "name": "User Name",
        "message": "Special characters: !@#$%^&*()_+",
        "unicode": "Hello 世界 こんにちは",
    }

    response = await client.post("/form", data=data)
    assert response.status_code == 200

    result = response.json()
    assert result["fields"] == data


async def test_single_file_upload(client):
    # Test uploading a single file with metadata
    file_content = "This is a test file content."
    files = {"document": ("test.txt", file_content, "text/plain")}
    data = {"description": "Test file upload", "category": "documentation"}

    response = await client.post("/upload", files=files, data=data)
    assert response.status_code == 200

    result = response.json()
    assert "files" in result
    assert "fields" in result

    # Check file data
    assert "document" in result["files"]
    uploaded_file = result["files"]["document"]
    assert uploaded_file["filename"] == "test.txt"
    assert uploaded_file["content"] == file_content
    assert uploaded_file["content_type"] == "text/plain"

    # Check form fields
    assert result["fields"]["description"] == "Test file upload"
    assert result["fields"]["category"] == "documentation"


async def test_multiple_file_uploads(client):
    # Test uploading multiple files of different types
    files = {
        "text_file": ("document.txt", "Text file content", "text/plain"),
        "json_file": ("data.json", '{"key": "value"}', "application/json"),
        "csv_file": ("data.csv", "name,age\nJohn,30\nJane,25", "text/csv"),
    }
    data = {"description": "Multiple files upload test"}

    response = await client.post("/upload", files=files, data=data)
    assert response.status_code == 200

    result = response.json()
    assert len(result["files"]) == 3

    # Check text file
    assert "text_file" in result["files"]
    text_file = result["files"]["text_file"]
    assert text_file["filename"] == "document.txt"
    assert text_file["content"] == "Text file content"
    assert text_file["content_type"] == "text/plain"

    # Check JSON file
    assert "json_file" in result["files"]
    json_file = result["files"]["json_file"]
    assert json_file["filename"] == "data.json"
    assert json_file["content"] == '{"key": "value"}'
    assert json_file["content_type"] == "application/json"

    # Check CSV file
    assert "csv_file" in result["files"]
    csv_file = result["files"]["csv_file"]
    assert csv_file["filename"] == "data.csv"
    assert csv_file["content"] == "name,age\nJohn,30\nJane,25"
    assert csv_file["content_type"] == "text/csv"

    # Check form field
    assert result["fields"]["description"] == "Multiple files upload test"


async def test_unicode_filename_and_content(client):
    # Test handling of Unicode in filenames and content
    filename = "测试文件.txt"
    content = "Unicode content: こんにちは世界 - Hello World!"

    files = {"unicode_file": (filename, content, "text/plain; charset=utf-8")}

    response = await client.post("/upload", files=files)
    assert response.status_code == 200

    result = response.json()
    assert "unicode_file" in result["files"]
    uploaded_file = result["files"]["unicode_file"]
    assert uploaded_file["filename"] == filename
    assert uploaded_file["content"] == content
    assert "text/plain" in uploaded_file["content_type"]


async def test_binary_file_upload(client):
    # Test binary file upload (using simplified binary content for test)
    # In a real scenario, this would be actual binary data
    binary_content = b"Binary content with \x00\x01\x02\x03 bytes"

    files = {"binary_file": ("data.bin", binary_content, "application/octet-stream")}

    response = await client.post("/upload", files=files)
    assert response.status_code == 200

    result = response.json()
    assert "binary_file" in result["files"]
    # Note: The test expects the binary content to be decoded as utf-8 with replacement
    # characters for invalid sequences - this is just for the test's sake
    # In a real app, you might want to handle binary data differently
