# -*- coding: utf-8; -*-

import datetime
import re
from unittest.mock import patch

import sqlalchemy as sa
from sqlalchemy import orm

from wuttjamaican.util import get_timezone_by_name

from wuttaweb import diffs as mod
from wuttaweb.testing import WebTestCase, VersionWebTestCase


class TestWebDiff(WebTestCase):

    def make_diff(self, *args, **kwargs):
        return mod.WebDiff(self.config, *args, **kwargs)

    def test_render_html(self):
        old_data = {"foo": "bar"}
        new_data = {"foo": "baz"}
        diff = self.make_diff(old_data, new_data)
        html = diff.render_html()
        self.assertIn("<table", html)
        self.assertIn("<tr>", html)
        self.assertIn("&#39;bar&#39;", html)
        self.assertIn(f'style="background-color: {diff.old_color}"', html)
        self.assertIn("&#39;baz&#39;", html)
        self.assertIn(f'style="background-color: {diff.new_color}"', html)
        self.assertIn("</tr>", html)
        self.assertIn("</table>", html)


class TestMergeDiff(WebTestCase):

    def make_diff(self, *args, **kwargs):
        return mod.MergeDiff(self.config, *args, **kwargs)

    def test_get_final_value_attrs(self):

        # no diff
        removing = {"foo": "bar"}
        keeping = {"foo": "bar"}
        final = {"foo": "bar"}
        is_diff = False
        diff = self.make_diff(removing, keeping, final)
        attrs = diff.get_final_value_attrs(is_diff)
        self.assertEqual(attrs, {})

        # values differ
        keeping = {"foo": "baz"}
        final = {"foo": "baz"}
        is_diff = True
        diff = self.make_diff(removing, keeping, final)
        attrs = diff.get_final_value_attrs(is_diff)
        self.assertEqual(attrs, {"class_": "has-background-warning"})

    def test_render_field_row(self):

        # no diffs
        removing = {"foo": "bar"}
        keeping = {"foo": "bar"}
        final = {"foo": "bar"}
        diff = self.make_diff(removing, keeping, final)
        html = diff.render_field_row("foo")
        self.assertTrue(html.startswith("<tr>"))
        self.assertNotIn('style="padding: 0.5rem;"', html)
        self.assertIn("&#39;bar&#39;", html)
        self.assertNotIn(f'style="background-color: {diff.old_color}"', html)
        self.assertNotIn("&#39;baz&#39;", html)
        self.assertNotIn(f'style="background-color: {diff.new_color}"', html)
        self.assertNotIn("&#39;bar,baz&#39;", html)
        self.assertNotIn('class="has-background-warning"', html)
        self.assertTrue(html.endswith("</tr>"))

        # remove vs. keep diffs, but not keep vs. final
        removing = {"foo": "bar"}
        keeping = {"foo": "baz"}
        final = {"foo": "baz"}
        diff = self.make_diff(removing, keeping, final)
        html = diff.render_field_row("foo")
        self.assertTrue(html.startswith("<tr>"))
        self.assertNotIn('style="padding: 0.5rem;"', html)
        self.assertIn("&#39;bar&#39;", html)
        self.assertIn(f'style="background-color: {diff.old_color}"', html)
        self.assertIn("&#39;baz&#39;", html)
        self.assertIn(f'style="background-color: {diff.new_color}"', html)
        self.assertNotIn("&#39;bar,baz&#39;", html)
        self.assertNotIn('class="has-background-warning"', html)
        self.assertTrue(html.endswith("</tr>"))

        # remove vs. keep diffs, *and* keep vs. final diffs
        removing = {"foo": "bar"}
        keeping = {"foo": "baz"}
        final = {"foo": "bar,baz"}
        diff = self.make_diff(removing, keeping, final)
        html = diff.render_field_row("foo")
        self.assertTrue(html.startswith("<tr>"))
        self.assertNotIn('style="padding: 0.5rem;"', html)
        self.assertIn("&#39;bar&#39;", html)
        self.assertIn(f'style="background-color: {diff.old_color}"', html)
        self.assertIn("&#39;baz&#39;", html)
        self.assertIn(f'style="background-color: {diff.new_color}"', html)
        self.assertIn("&#39;bar,baz&#39;", html)
        self.assertIn('class="has-background-warning"', html)
        self.assertTrue(html.endswith("</tr>"))

        # again, with cell padding
        diff.cell_padding = "0.5rem"
        html = diff.render_field_row("foo")
        self.assertTrue(html.startswith("<tr>"))
        self.assertIn('style="padding: 0.5rem"', html)
        self.assertIn("&#39;bar&#39;", html)
        self.assertIn(f"background-color: {diff.old_color}", html)
        self.assertIn("&#39;baz&#39;", html)
        self.assertIn(f"background-color: {diff.new_color}", html)
        self.assertIn("&#39;bar,baz&#39;", html)
        self.assertIn('class="has-background-warning"', html)
        self.assertTrue(html.endswith("</tr>"))


class TestVersionDiff(VersionWebTestCase):

    def make_diff(self, *args, **kwargs):
        return mod.VersionDiff(self.config, *args, **kwargs)

    def test_constructor(self):
        import sqlalchemy_continuum as continuum

        model = self.app.model
        user = model.User(username="fred")
        self.session.add(user)
        self.session.commit()
        user.username = "freddie"
        self.session.commit()
        self.session.delete(user)
        self.session.commit()

        txncls = continuum.transaction_class(model.User)
        vercls = continuum.version_class(model.User)
        versions = self.session.query(vercls).order_by(vercls.transaction_id).all()
        self.assertEqual(len(versions), 3)

        version = versions[0]
        diff = self.make_diff(version)
        self.assertEqual(diff.nature, "create")
        self.assertEqual(
            diff.fields,
            ["active", "person_uuid", "prevent_edit", "username", "uuid"],
        )

        version = versions[1]
        diff = self.make_diff(version)
        self.assertEqual(diff.nature, "update")
        self.assertEqual(
            diff.fields,
            ["active", "person_uuid", "prevent_edit", "username", "uuid"],
        )

        version = versions[2]
        diff = self.make_diff(version)
        self.assertEqual(diff.nature, "delete")
        self.assertEqual(
            diff.fields,
            ["active", "person_uuid", "prevent_edit", "username", "uuid"],
        )

    def test_render_version_value_with_objref(self):
        import sqlalchemy_continuum as continuum

        model = self.app.model
        person = model.Person(full_name="Fred Flintstone")
        self.session.add(person)

        # create, update, delete user
        user = model.User(username="fred", person=person)
        self.session.add(user)
        self.session.commit()
        user.username = "freddie"
        self.session.commit()
        self.session.delete(user)
        self.session.commit()

        txncls = continuum.transaction_class(model.User)
        vercls = continuum.version_class(model.User)
        versions = self.session.query(vercls).order_by(vercls.transaction_id).all()
        self.assertEqual(len(versions), 3)

        # create (1st version)
        version = versions[0]
        diff = self.make_diff(version)
        self.assertEqual(diff.nature, "create")
        self.assertEqual(diff.render_old_value("username"), "")
        self.assertIn("fred", diff.render_new_value("username"))
        self.assertNotIn("freddie", diff.render_new_value("username"))
        self.assertEqual(diff.render_old_value("person_uuid"), "")
        # rendered person_uuid includes display name
        html = diff.render_new_value("person_uuid")
        self.assertIn(str(person.uuid), html)
        self.assertIn("Fred Flintstone", html)

        # update (2nd version)
        version = versions[1]
        diff = self.make_diff(version)
        self.assertEqual(diff.nature, "update")
        self.assertIn("fred", diff.render_old_value("username"))
        self.assertNotIn("freddie", diff.render_old_value("username"))
        self.assertIn("freddie", diff.render_new_value("username"))
        # rendered person_uuid includes display name
        html = diff.render_old_value("person_uuid")
        self.assertIn(str(person.uuid), html)
        self.assertIn("Fred Flintstone", html)
        html = diff.render_new_value("person_uuid")
        self.assertIn(str(person.uuid), html)
        self.assertIn("Fred Flintstone", html)

        # delete (3rd version)
        version = versions[2]
        diff = self.make_diff(version)
        self.assertEqual(diff.nature, "delete")
        self.assertIn("freddie", diff.render_old_value("username"))
        self.assertEqual(diff.render_new_value("username"), "")
        # rendered person_uuid includes display name
        html = diff.render_old_value("person_uuid")
        self.assertIn(str(person.uuid), html)
        self.assertIn("Fred Flintstone", html)
        self.assertEqual(diff.render_new_value("person_uuid"), "")

    def test_render_version_value_with_datetime(self):
        import sqlalchemy_continuum as continuum

        tzlocal = get_timezone_by_name("America/Los_Angeles")
        with patch.object(self.app, "get_timezone", return_value=tzlocal):

            # make one person
            model = self.app.model
            person = model.Person(full_name="Fred Flintstone")
            self.session.add(person)
            self.session.commit()

            # get its one version record
            txncls = continuum.transaction_class(model.Person)
            vercls = continuum.version_class(model.Person)
            version = self.session.query(vercls).order_by(vercls.transaction_id).one()

            # make a diff, but we have to mock some things up for the test
            # coverage, since we don't currently have a versioned datetime
            # field in the base model.
            diff = self.make_diff(version)

            mock_column = sa.Column("timestamp", sa.DateTime())
            mock_property = orm.ColumnProperty(column=mock_column)

            class MockMapper:
                def __init__(self, mapper):
                    self.mapper = mapper

                def __getattr__(self, name):
                    if hasattr(self.mapper, name):
                        return getattr(self.mapper, name)
                    raise AttributeError(f"attr not found: {name}")

                def has_property(self, field):
                    if field == "timestamp":
                        return True
                    return self.mapper.has_property(field)

                def get_property(self, field):
                    if field == "timestamp":
                        return mock_property
                    return self.mapper.get_property(field)

            mock_mapper = MockMapper(diff.mapper)
            with patch.object(diff, "mapper", new=mock_mapper):

                # zone-aware local time
                dt = datetime.datetime(2026, 3, 14, 14, 0, tzinfo=tzlocal)
                html = diff.render_version_value(version, "timestamp", dt)
                self.assertTrue(
                    re.search(
                        r'<span style="[^"]+"><span title="[^"]+">\d{4}-\d{2}-\d{2} \d{2}:\d{2}-\d{4}</span></span>',
                        html,
                    )
                )
                self.assertTrue(
                    re.search(
                        r'<span style="[^"]+"><span title="[^"]+">2026-03-14 14:00-0700</span></span>',
                        html,
                    )
                )

                # zone-naive (presumed UTC)
                dt = datetime.datetime(2026, 3, 14, 21, 0)
                html = diff.render_version_value(version, "timestamp", dt)
                self.assertTrue(
                    re.search(
                        r'<span style="[^"]+"><span title="[^"]+">\d{4}-\d{2}-\d{2} \d{2}:\d{2}-\d{4}</span></span>',
                        html,
                    )
                )
                self.assertTrue(
                    re.search(
                        r'<span style="[^"]+"><span title="[^"]+">2026-03-14 14:00-0700</span></span>',
                        html,
                    )
                )

                # null
                html = diff.render_version_value(version, "timestamp", None)
                self.assertFalse(
                    re.search(
                        r'<span style="[^"]+"><span title="[^"]+">\d{4}-\d{2}-\d{2} \d{2}:\d{2}-\d{4}</span></span>',
                        html,
                    )
                )
