"""Performance benchmarks for GQLDB Python SDK.

Run with: pytest tests/benchmark/ -v --benchmark-only
Or manually: pytest tests/benchmark/test_benchmark.py -v
"""

import os
import time
import threading
import statistics
import pytest

from gqldb.config import ConfigBuilder
from gqldb.client import GqldbClient
from gqldb.types import NodeData
from gqldb.errors import GqldbError


# Test configuration
HOST = os.getenv("GQLDB_HOST", "192.168.1.104:60061")
USERNAME = os.getenv("GQLDB_USERNAME", "admin")
PASSWORD = os.getenv("GQLDB_PASSWORD", "root11")
GRAPH = os.getenv("GQLDB_TEST_GRAPH", "miniCircle")


@pytest.fixture(scope="module")
def client():
    """Create a connected client for benchmarks."""
    config = (
        ConfigBuilder()
        .hosts(HOST)
        .username(USERNAME)
        .password(PASSWORD)
        .default_graph(GRAPH)
        .timeout(60)
        .build()
    )
    c = GqldbClient(config)
    try:
        c.login(USERNAME, PASSWORD)
    except GqldbError as e:
        if "authentication is not enabled" not in str(e).lower():
            raise
    yield c
    c.close()


def measure_latency(func, iterations=100):
    """Measure latency statistics for a function."""
    latencies = []
    for _ in range(iterations):
        start = time.perf_counter()
        func()
        latencies.append((time.perf_counter() - start) * 1000)  # ms

    return {
        "min": min(latencies),
        "max": max(latencies),
        "mean": statistics.mean(latencies),
        "p50": statistics.median(latencies),
        "p95": sorted(latencies)[int(len(latencies) * 0.95)],
        "p99": sorted(latencies)[int(len(latencies) * 0.99)],
    }


# ==================== Latency Benchmarks ====================


@pytest.mark.benchmark
class TestLatencyBenchmarks:
    """Latency measurement benchmarks."""

    def test_simple_query_latency(self, client):
        """Measure simple query execution latency."""
        stats = measure_latency(lambda: client.gql("RETURN 1"))

        print(f"\nSimple Query Latency (ms):")
        print(f"  Min: {stats['min']:.2f}")
        print(f"  Max: {stats['max']:.2f}")
        print(f"  Mean: {stats['mean']:.2f}")
        print(f"  P50: {stats['p50']:.2f}")
        print(f"  P95: {stats['p95']:.2f}")
        print(f"  P99: {stats['p99']:.2f}")

        assert stats['mean'] < 1000  # Should be under 1 second

    def test_ping_latency(self, client):
        """Measure ping latency."""
        stats = measure_latency(lambda: client.ping())

        print(f"\nPing Latency (ms):")
        print(f"  Min: {stats['min']:.2f}")
        print(f"  Mean: {stats['mean']:.2f}")
        print(f"  P95: {stats['p95']:.2f}")

    def test_health_check_latency(self, client):
        """Measure health check latency."""
        stats = measure_latency(lambda: client.health_check())

        print(f"\nHealth Check Latency (ms):")
        print(f"  Min: {stats['min']:.2f}")
        print(f"  Mean: {stats['mean']:.2f}")
        print(f"  P95: {stats['p95']:.2f}")


# ==================== Throughput Benchmarks ====================


@pytest.mark.benchmark
class TestThroughputBenchmarks:
    """Throughput measurement benchmarks."""

    def test_query_throughput(self, client):
        """Measure queries per second."""
        iterations = 100
        start = time.perf_counter()

        for _ in range(iterations):
            client.gql("RETURN 1")

        elapsed = time.perf_counter() - start
        qps = iterations / elapsed

        print(f"\nQuery Throughput:")
        print(f"  Queries: {iterations}")
        print(f"  Time: {elapsed:.2f}s")
        print(f"  QPS: {qps:.2f}")

    def test_node_insert_throughput(self, client):
        """Measure node insertion throughput."""
        graph_name = f"bench_insert_{int(time.time() * 1000)}"

        try:
            client.gql(f"CREATE GRAPH {graph_name}")
        except GqldbError:
            pytest.skip("Cannot create test graph")
            return

        try:
            session = client.start_bulk_import(graph_name)

            try:
                iterations = 50
                start = time.perf_counter()

                for i in range(iterations):
                    node = NodeData(labels=["BenchNode"], properties={"id": i})
                    client.insert_nodes(graph_name, [node],
                                       bulk_import_session_id=session.session_id)



                elapsed = time.perf_counter() - start
                nps = iterations / elapsed

                print(f"\nSingle Node Insert Throughput:")
                print(f"  Nodes: {iterations}")
                print(f"  Time: {elapsed:.2f}s")
                print(f"  Nodes/sec: {nps:.2f}")
            finally:
                client.end_bulk_import(session.session_id)

        finally:
            try:
                client.gql(f"DROP GRAPH {graph_name}")
            except GqldbError:
                pass

    def test_batch_insert_throughput(self, client):
        """Measure batch node insertion throughput."""
        graph_name = f"bench_batch_{int(time.time() * 1000)}"
        batch_size = 100
        batches = 10

        try:
            client.gql(f"CREATE GRAPH {graph_name}")
        except GqldbError:
            pytest.skip("Cannot create test graph")
            return

        try:
            session = client.start_bulk_import(graph_name)

            try:
                start = time.perf_counter()

                for i in range(batches):
                    nodes = [
                        NodeData(labels=["BenchNode"], properties={"id": i * batch_size + j})
                        for j in range(batch_size)
                    ]
                    client.insert_nodes(graph_name, nodes,
                                       bulk_import_session_id=session.session_id)



                elapsed = time.perf_counter() - start
                total_nodes = batches * batch_size
                nps = total_nodes / elapsed

                print(f"\nBatch Insert Throughput:")
                print(f"  Batch size: {batch_size}")
                print(f"  Batches: {batches}")
                print(f"  Total nodes: {total_nodes}")
                print(f"  Time: {elapsed:.2f}s")
                print(f"  Nodes/sec: {nps:.2f}")
            finally:
                client.end_bulk_import(session.session_id)

        finally:
            try:
                client.gql(f"DROP GRAPH {graph_name}")
            except GqldbError:
                pass


# ==================== Scalability Benchmarks ====================


@pytest.mark.benchmark
class TestScalabilityBenchmarks:
    """Scalability measurement benchmarks."""

    def test_large_result_set_iteration(self, client):
        """Measure iteration over large result sets."""
        sizes = [100, 1000, 10000]

        for size in sizes:
            try:
                start = time.perf_counter()
                response = client.gql(f"MATCH (n) RETURN n LIMIT {size}")

                count = 0
                for row in response:
                    count += 1

                elapsed = time.perf_counter() - start

                print(f"\nResult Set Iteration (size={size}):")
                print(f"  Rows: {count}")
                print(f"  Time: {elapsed:.3f}s")
                if count > 0:
                    print(f"  Rows/sec: {count / elapsed:.2f}")

            except GqldbError as e:
                print(f"\nResult Set Iteration (size={size}): Skipped - {e}")

    def test_concurrent_queries(self, client):
        """Measure performance with concurrent queries."""
        concurrency_levels = [1, 5, 10]
        queries_per_thread = 20

        for concurrency in concurrency_levels:
            errors = []
            start = time.perf_counter()

            def worker():
                config = (
                    ConfigBuilder()
                    .hosts(HOST)
                    .username(USERNAME)
                    .password(PASSWORD)
                    .default_graph(GRAPH)
                    .timeout(60)
                    .build()
                )
                c = GqldbClient(config)
                try:
                    c.login(USERNAME, PASSWORD)
                except GqldbError as e:
                    if "authentication is not enabled" not in str(e).lower():
                        errors.append(str(e))
                        return

                try:
                    for _ in range(queries_per_thread):
                        c.gql("RETURN 1")
                except GqldbError as e:
                    errors.append(str(e))
                finally:
                    c.close()

            threads = [threading.Thread(target=worker) for _ in range(concurrency)]
            for t in threads:
                t.start()
            for t in threads:
                t.join()

            elapsed = time.perf_counter() - start
            total_queries = concurrency * queries_per_thread
            qps = total_queries / elapsed

            print(f"\nConcurrent Queries (concurrency={concurrency}):")
            print(f"  Total queries: {total_queries}")
            print(f"  Time: {elapsed:.2f}s")
            print(f"  QPS: {qps:.2f}")
            print(f"  Errors: {len(errors)}")


# ==================== Connection Benchmarks ====================


@pytest.mark.benchmark
class TestConnectionBenchmarks:
    """Connection benchmarks."""

    def test_connection_establishment_time(self):
        """Measure connection setup time."""
        iterations = 10
        latencies = []

        for _ in range(iterations):
            start = time.perf_counter()

            config = (
                ConfigBuilder()
                .hosts(HOST)
                .username(USERNAME)
                .password(PASSWORD)
                .default_graph(GRAPH)
                .timeout(30)
                .build()
            )
            c = GqldbClient(config)
            try:
                c.login(USERNAME, PASSWORD)
            except GqldbError:
                pass

            latencies.append((time.perf_counter() - start) * 1000)
            c.close()

        print(f"\nConnection Establishment (ms):")
        print(f"  Min: {min(latencies):.2f}")
        print(f"  Max: {max(latencies):.2f}")
        print(f"  Mean: {statistics.mean(latencies):.2f}")


# ==================== Transaction Benchmarks ====================


@pytest.mark.benchmark
class TestTransactionBenchmarks:
    """Transaction benchmarks."""

    def test_transaction_overhead(self, client):
        """Measure transaction begin/commit overhead."""
        iterations = 50

        # Without transaction
        start = time.perf_counter()
        for _ in range(iterations):
            client.gql("RETURN 1")
        without_tx = time.perf_counter() - start

        # With transaction
        start = time.perf_counter()
        for _ in range(iterations):
            tx = client.begin_transaction(GRAPH, read_only=True)
            from gqldb.client import QueryConfig
            client.gql("RETURN 1", config=QueryConfig(transaction_id=tx.id))
            client.commit(tx.id)
        with_tx = time.perf_counter() - start

        overhead = ((with_tx - without_tx) / without_tx) * 100

        print(f"\nTransaction Overhead:")
        print(f"  Without TX: {without_tx:.2f}s for {iterations} queries")
        print(f"  With TX: {with_tx:.2f}s for {iterations} queries")
        print(f"  Overhead: {overhead:.1f}%")
