#------------------------------------------------------------------------------
# Copyright (c) 2020, 2023, Oracle and/or its affiliates.
#
# This software is dual-licensed to you under the Universal Permissive License
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
# 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose
# either license.
#
# If you elect to accept the software under the Apache License, Version 2.0,
# the following applies:
#
# Licensed 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
#
#    https://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.
#------------------------------------------------------------------------------

"""
4000 - Module for testing the cursor executemany() method
"""

import decimal

import oracledb
import test_env

class TestCase(test_env.BaseTestCase):

    def test_4000_executemany_by_name(self):
        "4000 - test executing a statement multiple times (named args)"
        self.cursor.execute("truncate table TestTempTable")
        rows = [{"value": n} for n in range(250)]
        self.cursor.arraysize = 100
        statement = "insert into TestTempTable (IntCol) values (:value)"
        self.cursor.executemany(statement, rows)
        self.connection.commit()
        self.cursor.execute("select count(*) from TestTempTable")
        count, = self.cursor.fetchone()
        self.assertEqual(count, len(rows))

    def test_4001_executemany_by_position(self):
        "4001 - test executing a statement multiple times (positional args)"
        self.cursor.execute("truncate table TestTempTable")
        rows = [[n] for n in range(230)]
        self.cursor.arraysize = 100
        statement = "insert into TestTempTable (IntCol) values (:1)"
        self.cursor.executemany(statement, rows)
        self.connection.commit()
        self.cursor.execute("select count(*) from TestTempTable")
        count, = self.cursor.fetchone()
        self.assertEqual(count, len(rows))

    def test_4002_executemany_with_prepare(self):
        "4002 - test executing a statement multiple times (with prepare)"
        self.cursor.execute("truncate table TestTempTable")
        rows = [[n] for n in range(225)]
        self.cursor.arraysize = 100
        statement = "insert into TestTempTable (IntCol) values (:1)"
        self.cursor.prepare(statement)
        self.cursor.executemany(None, rows)
        self.connection.commit()
        self.cursor.execute("select count(*) from TestTempTable")
        count, = self.cursor.fetchone()
        self.assertEqual(count, len(rows))

    def test_4003_executemany_with_rebind(self):
        "4003 - test executing a statement multiple times (with rebind)"
        self.cursor.execute("truncate table TestTempTable")
        rows = [[n] for n in range(235)]
        self.cursor.arraysize = 100
        statement = "insert into TestTempTable (IntCol) values (:1)"
        self.cursor.executemany(statement, rows[:50])
        self.cursor.executemany(statement, rows[50:])
        self.connection.commit()
        self.cursor.execute("select count(*) from TestTempTable")
        count, = self.cursor.fetchone()
        self.assertEqual(count, len(rows))

    def test_4004_executemany_with_input_sizes_wrong(self):
        "4004 - test executing multiple times (with input sizes wrong)"
        cursor = self.connection.cursor()
        cursor.setinputsizes(oracledb.NUMBER)
        data = [[decimal.Decimal("25.8")], [decimal.Decimal("30.0")]]
        cursor.executemany("declare t number; begin t := :1; end;", data)

    def test_4005_executemany_with_multiple_batches(self):
        "4005 - test executing multiple times (with multiple batches)"
        self.cursor.execute("truncate table TestTempTable")
        sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)"
        self.cursor.executemany(sql, [(1, None), (2, None)])
        self.cursor.executemany(sql, [(3, None), (4, "Testing")])

    def test_4006_executemany_numeric(self):
        "4006 - test executemany() with various numeric types"
        self.cursor.execute("truncate table TestTempTable")
        data = [
            (1, 5),
            (2, 7.0),
            (3, 6.5),
            (4, 2 ** 65),
            (5, decimal.Decimal("24.5"))
        ]
        sql = "insert into TestTempTable (IntCol, NumberCol) values (:1, :2)"
        self.cursor.executemany(sql, data)
        self.cursor.execute("""
                select IntCol, NumberCol
                from TestTempTable
                order by IntCol""")
        self.assertEqual(self.cursor.fetchall(), data)

    def test_4007_executemany_with_resize(self):
        "4007 - test executing a statement multiple times (with resize)"
        self.cursor.execute("truncate table TestTempTable")
        rows = [
            (1, "First"),
            (2, "Second"),
            (3, "Third"),
            (4, "Fourth"),
            (5, "Fifth"),
            (6, "Sixth"),
            (7, "Seventh and the longest one")
        ]
        sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)"
        self.cursor.executemany(sql, rows)
        self.cursor.execute("""
                select IntCol, StringCol1
                from TestTempTable
                order by IntCol""")
        fetched_rows = self.cursor.fetchall()
        self.assertEqual(fetched_rows, rows)

    def test_4008_executemany_with_exception(self):
        "4008 - test executing a statement multiple times (with exception)"
        self.cursor.execute("truncate table TestTempTable")
        rows = [{"value": n} for n in (1, 2, 3, 2, 5)]
        statement = "insert into TestTempTable (IntCol) values (:value)"
        self.assertRaisesRegex(oracledb.DatabaseError, "^ORA-00001:",
                               self.cursor.executemany, statement, rows)
        self.assertEqual(self.cursor.rowcount, 3)

    def test_4009_executemany_with_invalid_parameters(self):
        "4009 - test calling executemany() with invalid parameters"
        sql = "insert into TestTempTable (IntCol, StringCol1) values (:1, :2)"
        self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2004:",
                               self.cursor.executemany, sql,
                               "These are not valid parameters")

    def test_4010_executemany_no_parameters(self):
        "4010 - test calling executemany() without any bind parameters"
        num_rows = 5
        self.cursor.execute("truncate table TestTempTable")
        self.cursor.executemany("""
                declare
                    t_Id          number;
                begin
                    select nvl(count(*), 0) + 1 into t_Id
                    from TestTempTable;

                    insert into TestTempTable (IntCol, StringCol1)
                    values (t_Id, 'Test String ' || t_Id);
                end;""", num_rows)
        self.assertEqual(self.cursor.rowcount, 0)
        self.cursor.execute("select count(*) from TestTempTable")
        count, = self.cursor.fetchone()
        self.assertEqual(count, num_rows)

    def test_4011_executemany_bound_earlier(self):
        "4011 - test calling executemany() with binds performed earlier"
        num_rows = 9
        self.cursor.execute("truncate table TestTempTable")
        var = self.cursor.var(int, arraysize=num_rows)
        self.cursor.setinputsizes(var)
        self.cursor.executemany("""
                declare
                    t_Id          number;
                begin
                    select nvl(count(*), 0) + 1 into t_Id
                    from TestTempTable;

                    insert into TestTempTable (IntCol, StringCol1)
                    values (t_Id, 'Test String ' || t_Id);

                    select sum(IntCol) into :1
                    from TestTempTable;
                end;""", num_rows)
        self.assertEqual(self.cursor.rowcount, 0)
        expected_data = [1, 3, 6, 10, 15, 21, 28, 36, 45]
        self.assertEqual(var.values, expected_data)

    def test_4012_executemany_with_plsql_binds(self):
        "4012 - test executing plsql statements multiple times (with binds)"
        var = self.cursor.var(int, arraysize=5)
        self.cursor.setinputsizes(var)
        data = [[25], [30], [None], [35], [None]]
        exepected_data = [25, 30, None, 35, None]
        self.cursor.executemany("declare t number; begin t := :1; end;", data)
        self.assertEqual(var.values, exepected_data)

    def test_4013_executemany_with_incorrect_params(self):
        "4013 - test executemany with incorrect parameters"
        self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2004:",
                               self.cursor.executemany, "select :1 from dual",
                               [1])

    def test_4014_executemany_with_mixed_binds_pos_first(self):
        "4014 - test executemany with mixed binds (pos first)"
        rows = [["test"], {"value": 1}]
        self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2006:",
                               self.cursor.executemany, "select :1 from dual",
                               rows)

    def test_4015_executemany_with_mixed_binds_name_first(self):
        "4015 - test executemany with mixed binds (name first)"
        rows = [{"value": 1}, ["test"]]
        self.assertRaisesRegex(oracledb.ProgrammingError, "^DPY-2006:",
                               self.cursor.executemany,
                               "select :value from dual", rows)

    def test_4016_executemany_plsql_dml_returning(self):
        "4016 - test executemany() with a pl/sql statement with dml returning"
        num_rows = 5
        self.cursor.execute("truncate table TestTempTable")
        out_var = self.cursor.var(oracledb.NUMBER, arraysize=5)
        self.cursor.setinputsizes(out_var)
        self.cursor.executemany("""
                declare
                    t_Id          number;
                begin
                    select nvl(count(*), 0) + 1 into t_Id
                    from TestTempTable;

                    insert into TestTempTable (IntCol, StringCol1)
                    values (t_Id, 'Test String ' || t_Id) returning
                    IntCol into :out_bind;
                end;""", num_rows)
        self.assertEqual(out_var.values, [1, 2, 3, 4, 5])

    def test_4017_executemany_pl_sql_with_in_and_out_binds(self):
        "4017 - test executemany() with pl/sql in binds and out binds"
        self.cursor.execute("truncate table TestTempTable")
        int_values = [5, 8, 17, 24, 6]
        str_values = ["Test 5", "Test 8", "Test 17", "Test 24", "Test 6"]
        out_bind = self.cursor.var(oracledb.NUMBER, arraysize=5)
        self.cursor.setinputsizes(None, None, out_bind)
        data = list(zip(int_values, str_values))
        self.cursor.executemany("""
            begin
                insert into TestTempTable (IntCol, StringCol1)
                values (:int_val, :str_val)
                returning IntCol into :out_bind;
            end;""", data)
        self.assertEqual(out_bind.values, [5, 8, 17, 24, 6])

    def test_4018_executemany_pl_sql_out_bind(self):
        "4018 - test executemany() with pl/sql outbinds"
        self.cursor.execute("truncate table TestTempTable")
        out_bind = self.cursor.var(oracledb.NUMBER, arraysize=5)
        self.cursor.setinputsizes(out_bind)
        self.cursor.executemany("""
            begin
                :out_var := 5;
            end;""", 5)
        self.assertEqual(out_bind.values, [5, 5, 5, 5, 5])

    def test_4019_re_executemany_pl_sql_with_in_and_out_binds(self):
        "4019 - test re-executemany() with pl/sql in binds and out binds"
        int_values = [5, 8, 17, 24, 6]
        str_values = ["Test 5", "Test 8", "Test 17", "Test 24", "Test 6"]
        data = list(zip(int_values, str_values))
        for i in range(2):
            self.cursor.execute("truncate table TestTempTable")
            out_bind = self.cursor.var(oracledb.NUMBER, arraysize=5)
            self.cursor.setinputsizes(None, None, out_bind)
            self.cursor.executemany("""
                begin
                    insert into TestTempTable (IntCol, StringCol1)
                    values (:int_val, :str_val)
                    returning IntCol into :out_bind;
                end;""", data)
            self.assertEqual(out_bind.values, [5, 8, 17, 24, 6])

    def test_4020_executemany_with_plsql_single_row(self):
        "4020 - test PL/SQL statement with single row bind"
        value = 4020
        var = self.cursor.var(int)
        data = [[var, value]]
        self.cursor.executemany("begin :1 := :2; end;", data)
        self.assertEqual(var.values, [value])

    def test_4021_defer_type_assignment(self):
        "4021 - test deferral of type assignment"
        self.cursor.execute("truncate table TestTempTable")
        data = [(1, None), (2, 25)]
        self.cursor.executemany("""
                insert into TestTempTable
                (IntCol, NumberCol)
                values (:1, :2)""", data)
        self.connection.commit()
        self.cursor.execute("""
                select IntCol, NumberCol
                from TestTempTable
                order by IntCol""")
        fetched_data = self.cursor.fetchall()
        self.assertEqual(data, fetched_data)

    def test_4022_plsql_large_number_of_binds(self):
        "4022 - test PL/SQL with a lerge number of binds"
        parts = []
        bind_names = []
        all_bind_values = []
        out_binds = []
        for i in range(5):
            all_bind_values.append([])
        for i in range(350):
            n = len(parts) + 1
            bind_names.extend([f"v_out_{n}_0", f"a_{n}", f"b_{n}", f"c_{n}"])
            parts.append(f":v_out{n} := :a_{n} + :b_{n} + :c_{n};")
            out_binds.append(self.cursor.var(int,
                                             arraysize=len(all_bind_values)))
            for j, bind_values in enumerate(all_bind_values):
                bind_values.extend([out_binds[-1], n * 1 + j, n * 2 + j,
                                    n * 3 + j])
        lf = "\n"
        sql = f"begin{lf}{lf.join(parts)}{lf}end;"
        self.cursor.executemany(sql, all_bind_values)
        init_val = 6
        for var in out_binds:
            expected = [init_val, init_val + 3, init_val + 6, init_val + 9,
                        init_val + 12]
            self.assertEqual(var.values, expected)
            init_val += 6

if __name__ == "__main__":
    test_env.run_test_cases()
