# Copyright 2025-present ScyllaDB
#
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0

#############################################################################
# Tests for limits on the *names* of keyspaces, tables, indexes, views, function and columns.
#############################################################################

import pytest
from contextlib import contextmanager
from cassandra.protocol import InvalidRequest
from .util import unique_name, new_test_table, new_secondary_index, new_function, is_scylla

# This context manager is similar to new_test_table() - creating a table and
# keeping it alive while the context manager is in scope - but allows the
# caller to pick the name of the table. This is useful to attempt to create
# a table whose name has different lengths or characters.
# Note that if used in a shared keyspace, it is recommended to base the
# given table name on the output of unique_name(), to avoid name clashes.
# See the padded_name() function below as an example.
@contextmanager
def new_named_table(cql, keyspace, table, schema, extra=""):
    tbl  = keyspace+'.'+table
    cql.execute(f'CREATE TABLE {tbl} ({schema}) {extra}')
    try:
        yield tbl
    finally:
        cql.execute(f'DROP TABLE {tbl}')

# padded_name() creates a unique name of given length by taking the
# output of unique_name() and padding it with extra 'x' characters:
def padded_name(length):
    u = unique_name()
    assert length >= len(u)
    return u + 'x'*(length-len(u))

# Utility function to create a new keyspace with the given name.
# Created to avoid passing the same replication option in every tests.
@contextmanager
def new_keyspace(cql, ks_name=unique_name()):
    cql.execute(f"CREATE KEYSPACE {ks_name} WITH REPLICATION = {{'class': 'SimpleStrategy', 'replication_factor': 1}}")
    try:
        yield ks_name
    finally:
        cql.execute(f"DROP KEYSPACE {ks_name}")

# Utility function to create a new table with the given name.
# Created to avoid passing the same schema option in every tests.
@contextmanager
def new_table(cql, ks_name, tbl_name=unique_name(), extra=""):
    qualified_table_name = ks_name + '.' + tbl_name
    cql.execute(f'CREATE TABLE {qualified_table_name} (p int, x int, PRIMARY KEY (p)) {extra}')
    try:
        yield qualified_table_name
    finally:
        cql.execute(f'DROP TABLE {qualified_table_name}')

# Utility function to create a materialized view with the given name.
# Created to avoid passing the same parameter values in every tests.
@contextmanager
def new_mv(cql, qualified_table_name, mv_name):
    keyspace = qualified_table_name.split('.')[0]
    qualified_mv_name = keyspace + "." + mv_name
    cql.execute(
        f"CREATE MATERIALIZED VIEW {qualified_mv_name} AS SELECT * FROM {qualified_table_name} WHERE p is not null and x is not null PRIMARY KEY (p, x)"
    )
    try:
        yield qualified_mv_name
    finally:
        cql.execute(f"DROP MATERIALIZED VIEW {qualified_mv_name}")

# Cassandra's documentation states that "Both keyspace and table name ... are
# limited in size to 48 characters". This was actually only true in Cassandra
# 3, and by Cassandra 4 and 5 this limitation was dropped (see discussion
# in CASSANDRA-20425).
# Test verifies that a 48-character name is allowed, and passes on all versions
# of Scylla and Cassandra.
def test_table_name_length_48(cql, test_keyspace):
    with new_table(cql, test_keyspace, padded_name(48)):
        pass

# After Cassandra removed the 48-character limit for table names, some applications began creating tables with longer names.
# ScyllaDB relaxed its own limit to maintain compatibility with these applications.
# Due to CDC-enabled tables, ScyllaDB enforces a 192-character limit (see schema::NAME_LENGTH for details),
# while Cassandra allows names up to 222 characters.
# The 192-character limit in ScyllaDB is considered sufficient for most use cases.
# Test verifies that a 192-character name is allowed, and passes on Scylla and latest Cassandra versions (4 and 5).
def test_table_name_length_192(cql, test_keyspace):
    with new_table(cql, test_keyspace, padded_name(192)):
        pass

# If we try an even longer table name length, e.g., 500 characters, we run
# into the problem that an attempt to create a file or directory name based
# on the table name will fail (due to filesystem capabilities). Even if we lift the 48-character limitation
# introduced in Cassandra 3, creating a 500-character name should fail gracefully.
# Test verifies that a 500-character name is rejected by ScyllaDB and Cassandra.
# We mark this test cassandra_bug because Cassandra 5 hangs on this test (CASSANDRA-20425 and CASSANDRA-20389).
def test_table_name_length_500(cql, test_keyspace, cassandra_bug):
    name = padded_name(500)
    with pytest.raises(InvalidRequest, match=name):
        with new_table(cql, test_keyspace, name):
            pass

# Test verifies that a 192-character name for CDC enabled table is accepted for Scylla and Cassandra 4/5.
def test_table_cdc_name_length_192(cql):
    # Incompatible Cassandra <-> Scylla API to enable CDC. See #9859.
    cdc = "{'enabled': true}" if is_scylla(cql) else 'true'
    with new_keyspace(cql) as keyspace:
        with new_table(cql, keyspace, padded_name(192), extra=f"with CDC = {cdc}"):
            pass

# When ScyllaDB lifted the table name length restriction, it also lifted the limit on keyspace name length.
# However, Cassandra still enforces a 48-character limit for keyspace names.
# Test verifies that a keyspace name of exactly 48 characters is accepted by all versions of ScyllaDB and Cassandra.
def test_keyspace_name_length_48(cql):
    with new_keyspace(cql, padded_name(48)):
        pass

# Test verifies that a keyspace name of exactly 192 is accepted by ScyllaDB.
# Marked scylla_only because Cassandra keeps the 48-character limit for keyspace names.
def test_keyspace_name_length_192(cql, scylla_only):
    with new_keyspace(cql, padded_name(192)):
        pass

# Test verifies that too long keyspace (exceeding filesystem capabilities) is gracefully rejected.
def test_keyspace_name_length_500(cql):
    name = padded_name(500)
    with pytest.raises(InvalidRequest, match=name):
        with new_keyspace(cql, name):
            pass

# Test verifies that materialized view names follow the same length rules as table names.
def test_mv_name_length_192(cql, test_keyspace):
    with new_table(cql, test_keyspace) as table:
        with new_mv(cql, table, padded_name(192)):
            pass

# Test verifies that materialized view names follow the same length rules as table names.
# Marked cassandra_bug because Cassandra 5 hangs on this test (CASSANDRA-20425 and CASSANDRA-20389).
def test_mv_name_length_500(cql, test_keyspace, cassandra_bug):
    name = padded_name(500)
    with new_table(cql, test_keyspace) as table:
        with pytest.raises(InvalidRequest, match=name):
            with new_mv(cql, table, name):
                pass

# Test verifies that secondary index names follow the same length rules as table names.
def test_index_name_length_192(cql, test_keyspace):
    with new_table(cql, test_keyspace) as table:
        with new_secondary_index(cql, table, "x", padded_name(192)):
            pass

# Test verifies that secondary index names follow the same length rules as table names.
# Marked cassandra_bug because Cassandra 5 hangs on this test (CASSANDRA-20425 and CASSANDRA-20389).
def test_index_name_length_500(cql, test_keyspace, cassandra_bug):
    name = padded_name(500)
    with new_table(cql, test_keyspace) as table:
        with pytest.raises(InvalidRequest, match=name):
            with new_secondary_index(cql, table, "x", name):
                pass

# Test verifies that column names are not restricted by schema::NAME_LENGTH or filesystem constraints.
def test_function_name_500(cql, test_keyspace):
    # ScyllaDB by default supports Lua functions while Cassandra Java only.
    # In this test we only want to verify function name, hence function body is not important.
    lang = "lua" if is_scylla(cql) else "java"
    with new_function(cql, test_keyspace, f"() CALLED ON NULL INPUT RETURNS int LANGUAGE {lang} AS 'return 0;'",
                      name=padded_name(500)):
        pass

# Test verifies that column names are not restricted by schema::NAME_LENGTH or filesystem constraints.
def test_column_name_500(cql, test_keyspace):
    with new_test_table(cql, test_keyspace, "p int primary key, " + padded_name(500) + " int"):
        pass

# TODO: add tests for the allowed characters in a table name
