# Cherrypy-foundation
# Copyright (C) 2022-2026 IKUS Software
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
import os
import tempfile
from urllib.parse import urlencode

import cherrypy
from cherrypy.test import helper
from parameterized import parameterized_class
from sqlalchemy import Column, Integer, String, func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.sql.schema import Index

from .. import db  # noqa

Base = cherrypy.db.base


class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    username = Column(String)

    def __repr__(self):
        return f"User(id={self.id}, username='{self.username}')"


Index('user_username_unique_ix', func.lower(User.username), unique=True, info='This username already exists.')


class Root:

    @cherrypy.expose
    def index(self):
        sess = cherrypy.db.session
        with sess.begin():
            users = sess.execute(select(User)).scalars().all()
            return str(users)

    @cherrypy.expose
    def add(self, username):
        try:
            sess = cherrypy.db.session
            with sess.begin():
                sess.add(User(username=username))
                return "OK"
        except IntegrityError as e:
            if e.constraint:
                return f"{e} - {e.constraint.info}"
            else:
                return str(e)

    @cherrypy.expose
    def add_implicit(self, username):
        # Implicit transaction
        sess = cherrypy.db.session
        sess.add(User(username=username))
        sess.flush()
        return "OK"

    @cherrypy.expose
    def unhandled_error(self, username):
        # Unhandled errors are rolled back.
        sess = cherrypy.db.session
        sess.add(User(username=username))
        sess.flush()
        raise ValueError('server error')

    @cherrypy.expose
    def http_error(self, username):
        # CherryPy HTTP errors are committed.
        sess = cherrypy.db.session
        sess.add(User(username=username))
        sess.flush()
        raise cherrypy.HTTPError(400)


@parameterized_class(
    ('autobegin',),
    [
        (True,),
        (False,),
    ],
)
class DbPluginTest(helper.CPWebCase):
    interactive = False
    autobegin = None

    @classmethod
    def setup_class(cls):
        cls.tempdir = tempfile.TemporaryDirectory(prefix='cherrypy-foundation-', suffix='-db-test')
        super().setup_class()

    @classmethod
    def teardown_class(cls):
        cls.tempdir.cleanup()
        super().teardown_class()

    @classmethod
    def setup_server(cls):
        db_uri = os.environ.get('TEST_DATABASE_URI', f"sqlite:///{cls.tempdir.name}/data.db")
        cherrypy.config.update(
            {
                'db.uri': db_uri,
                'db.autobegin': cls.autobegin,
            }
        )
        cherrypy.tree.mount(Root(), '/')

    def setUp(self):
        cherrypy.db.clear_sessions()
        with cherrypy.db.session.begin():
            cherrypy.db.create_all(commit=False)
        return super().setUp()

    def tearDown(self):
        with cherrypy.db.session.begin():
            cherrypy.db.drop_all(commit=False)
        return super().tearDown()

    def _count_users(self):
        return cherrypy.db.session.execute(select(func.count()).select_from(User)).scalar()

    def test_add_user(self):
        # Given an empty database
        with cherrypy.db.session.begin():
            self.assertEqual(0, self._count_users())
        # When calling "/add" to create a user
        self.getPage('/add', method='POST', body=urlencode({'username': 'myuser'}))
        self.assertStatus(200)
        self.assertInBody('OK')
        # Then user get added to database
        with cherrypy.db.session.begin():
            self.assertEqual(1, self._count_users())

    def test_add_duplicate_user(self):
        # Given a database with a user
        with cherrypy.db.session.begin():
            cherrypy.db.session.add(User(username='user1'))
        # When trying to add another user with the same username
        self.getPage('/add', method='POST', body=urlencode({'username': 'user1'}))
        # Then an error get raised
        self.assertStatus(200)
        self.assertInBody('user_username_unique_ix')
        # Then error also make reference to the contraints info.
        self.assertInBody('This username already exists.')

    def test_get_users(self):
        # Given a database with a user
        with cherrypy.db.session.begin():
            new_user = User(username='newuser')
            cherrypy.db.session.add(new_user)
        # When calling "/"
        self.getPage("/")
        self.assertStatus(200)
        # Then the page includes our user
        with cherrypy.db.session.begin():
            self.assertInBody(new_user.username)

    def test_implicit_add(self):
        if self.autobegin is False:
            self.skip('this only works for implicit begin session')
        # Given an empty database
        with cherrypy.db.session.begin():
            self.assertEqual(0, self._count_users())
        # When calling "/add-implicit" to create a user
        self.getPage('/add-implicit', method='POST', body=urlencode({'username': 'myuser'}))
        self.assertStatus(200)
        self.assertInBody('OK')
        # Then user get added to database (commit occurs in `after_request`)
        with cherrypy.db.session.begin():
            self.assertEqual(1, self._count_users())

    def test_implicit_rollback_on_unhandled_error(self):
        if self.autobegin is False:
            self.skip('this only works for implicit begin session')
        # Given an empty database
        with cherrypy.db.session.begin():
            self.assertEqual(0, self._count_users())
        # When calling "/unhandled-error"
        self.getPage('/unhandled-error', method='POST', body=urlencode({'username': 'myuser'}))
        self.assertStatus(500)
        # Then user is not added (rollback occurs in `after_request`)
        with cherrypy.db.session.begin():
            self.assertEqual(0, self._count_users())

    def test_implicit_commit_on_http_error(self):
        if self.autobegin is False:
            self.skip('this only works for implicit begin session')
        # Given an empty database
        with cherrypy.db.session.begin():
            self.assertEqual(0, self._count_users())
        # When calling "/http-error"
        self.getPage('/http-error', method='POST', body=urlencode({'username': 'myuser'}))
        self.assertStatus(400)
        # Then user is added (commit occurs in `after_request`)
        with cherrypy.db.session.begin():
            self.assertEqual(1, self._count_users())

    def test_get_changes(self):
        # New
        with cherrypy.db.session.begin():
            user = User(username='foo').add()
            self.assertEqual(('new', {'username': [None, 'foo']}), user.get_changes())
        # Clean
        with cherrypy.db.session.begin():
            self.assertEqual(('clean', {}), user.get_changes())
        # Dirty
        with cherrypy.db.session.begin():
            user = User.query.one()
            user.username = 'bar'
            self.assertEqual(('dirty', {'username': ['foo', 'bar']}), user.get_changes())
        # Deleted
        with cherrypy.db.session.begin():
            user = User.query.one()
            user.delete()
            self.assertEqual(('deleted', {}), user.get_changes())

    def test_regex(self):
        # Make sure our regex function is working.
        with cherrypy.db.session.begin():
            User(username='foo').add().flush()
            user = User.query.filter(User.username.regexp_match("^f")).all()
            self.assertTrue(user)

    def test_regexp_replace(self):
        with cherrypy.db.session.begin():
            User(username='foobar').add().flush()
            result = (
                User.query.filter(User.username == 'foobar')
                .with_entities(func.regexp_replace(User.username, 'foo', 'baz'))
                .scalar()
            )
            self.assertEqual(result, 'bazbar')

    def test_split_part(self):
        with cherrypy.db.session.begin():
            User(username='foo.bar.baz').add().flush()
            result = (
                User.query.filter(User.username == 'foo.bar.baz')
                .with_entities(func.split_part(User.username, '.', 2))
                .scalar()
            )
            self.assertEqual(result, 'bar')

    def test_reverse(self):
        with cherrypy.db.session.begin():
            User(username='foobar').add().flush()
            result = User.query.filter(User.username == 'foobar').with_entities(func.reverse(User.username)).scalar()
            self.assertEqual(result, 'raboof')
