# Part of Odoo. See LICENSE file for full copyright and licensing details.

from freezegun import freeze_time
from unittest.mock import patch

import odoo

from odoo import Command
from odoo.addons.test_http.utils import (
    TEST_IP,
    USER_AGENT_android_chrome,
    USER_AGENT_linux_chrome,
    USER_AGENT_linux_firefox
)
from .test_common import TestHttpBase


class TestDevice(TestHttpBase):

    def setUp(self):
        super().setUp()

        self.Device = self.env['res.device']
        self.DeviceLog = self.env['res.device.log']
        self.DeviceLog.search([]).unlink()

        self.user_admin = self.env.ref('base.user_admin')
        self.user_internal = self.env['res.users'].create({
            'login': 'internal',
            'password': 'internal',
            'name': 'Internal',
            'email': 'internal@example.com',
            'groups_id': [Command.set([self.env.ref('base.group_user').id])],
        })

    def hit(self, time, endpoint, headers=None, ip=None):
        if ip:
            headers = headers or {}
            headers = {
                **headers,
                'Host': '',
                'X-Forwarded-For': ip,
                'X-Forwarded-Host': 'odoo.com',
                'X-Forwarded-Proto': 'https'
            }
        with freeze_time(time), \
            patch.dict(odoo.tools.config.options, {'proxy_mode': bool(ip)}):
            res = self.url_open(url=endpoint, headers=headers)
        return res

    def info_trace(self, trace):
        return {
            'elapsed_time': trace['last_activity'] - trace['first_activity'],
            'platform': trace['platform'],
            'browser': trace['browser'],
            'ip_address': trace['ip_address'],
        }

    def get_devices_logs(self, user=None):
        domain = [('user_id', '=', user.id)] if user else []
        devices = self.Device.search(domain)
        logs = self.DeviceLog.search([
            ('session_identifier', 'in', devices.mapped('session_identifier')),
            ('platform', 'in', devices.mapped('platform')),
            ('browser', 'in', devices.mapped('browser'))
        ])
        return devices, logs

    # --------------------
    # DETECTION
    # --------------------

    def test_detection_device_readonly(self):
        session = self.authenticate(self.user_admin.login, self.user_admin.login)
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public')

        devices, logs = self.get_devices_logs(self.user_admin)
        self.assertEqual(len(devices), 1)
        self.assertEqual(len(logs), 1)
        self.assertEqual(len(session._trace), 1)

    def test_detection_device_no_readonly(self):
        session = self.authenticate(self.user_admin.login, self.user_admin.login)
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0')

        devices, logs = self.get_devices_logs(self.user_admin)
        self.assertEqual(len(devices), 1)
        self.assertEqual(len(logs), 1)
        self.assertEqual(len(session._trace), 1)

    def test_detection_user_public(self):
        self.authenticate(None, None)
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0')

        devices, logs = self.get_devices_logs()
        self.assertEqual(len(devices), 0)
        self.assertEqual(len(logs), 0)

    def test_detection_device_readonly_then_no_readonly(self):
        session = self.authenticate(self.user_admin.login, self.user_admin.login)
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public')

        devices, logs = self.get_devices_logs(self.user_admin)
        self.assertEqual(len(devices), 1)
        self.assertEqual(len(logs), 1)
        self.assertEqual(len(session._trace), 1)

        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0')

        devices, logs = self.get_devices_logs(self.user_admin)
        self.assertEqual(len(devices), 1)
        self.assertEqual(len(logs), 1)
        self.assertEqual(len(session._trace), 1)

    def test_detection_device_according_to_time(self):
        session = self.authenticate(self.user_admin.login, self.user_admin.login)
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0')

        devices, logs = self.get_devices_logs(self.user_admin)
        self.assertEqual(len(devices), 1)
        self.assertEqual(len(logs), 1)
        self.assertEqual(len(session._trace), 1)
        self.assertEqual(self.info_trace(session._trace[0])['elapsed_time'], 0)

        self.hit('2024-01-01 08:30:00', '/test_http/greeting-public?readonly=0')

        devices, logs = self.get_devices_logs(self.user_admin)
        self.assertEqual(len(devices), 1)
        self.assertEqual(len(logs), 1)
        self.assertEqual(len(session._trace), 1)
        self.assertEqual(self.info_trace(session._trace[0])['elapsed_time'], 0)  # No trace update (< 3600 sec)

        self.hit('2024-01-01 09:00:00', '/test_http/greeting-public?readonly=0')

        devices, logs = self.get_devices_logs(self.user_admin)
        self.assertEqual(len(devices), 1)
        self.assertEqual(len(logs), 2)
        self.assertEqual(len(session._trace), 1)
        self.assertEqual(self.info_trace(session._trace[0])['elapsed_time'], 3600)

        self.hit('2024-01-01 10:00:00', '/test_http/greeting-public?readonly=0')

        devices, logs = self.get_devices_logs(self.user_admin)
        self.assertEqual(len(devices), 1)
        self.assertEqual(len(logs), 3)
        self.assertEqual(len(session._trace), 1)
        self.assertEqual(self.info_trace(session._trace[0])['elapsed_time'], 7200)

    def test_detection_device_according_to_useragent(self):
        session = self.authenticate(self.user_admin.login, self.user_admin.login)

        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome})

        devices, logs = self.get_devices_logs(self.user_admin)
        self.assertEqual(len(devices), 1)
        self.assertEqual(len(logs), 1)
        self.assertEqual(len(session._trace), 1)
        self.assertEqual(self.info_trace(session._trace[0])['platform'], 'linux')
        self.assertEqual(self.info_trace(session._trace[0])['browser'], 'chrome')

        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_firefox})

        devices, logs = self.get_devices_logs(self.user_admin)
        self.assertEqual(len(devices), 2)
        self.assertEqual(len(logs), 2)
        self.assertEqual(len(session._trace), 2)
        self.assertEqual(self.info_trace(session._trace[1])['platform'], 'linux')
        self.assertEqual(self.info_trace(session._trace[1])['browser'], 'firefox')

    def test_detection_device_according_to_ipaddress(self):
        session = self.authenticate(self.user_admin.login, self.user_admin.login)
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0')

        devices, logs = self.get_devices_logs(self.user_admin)
        self.assertEqual(len(devices), 1)
        self.assertEqual(len(logs), 1)
        self.assertEqual(len(session._trace), 1)

        self.hit('2024-01-01 08:00:01', '/test_http/greeting-public?readonly=0', ip=TEST_IP)

        devices, logs = self.get_devices_logs(self.user_admin)
        self.assertEqual(len(devices), 1)
        self.assertEqual(len(logs), 2)
        self.assertEqual(len(session._trace), 2)
        self.assertNotEqual(self.info_trace(session._trace[0])['ip_address'], TEST_IP)
        self.assertEqual(self.info_trace(session._trace[1])['ip_address'], TEST_IP)

        localized_device = devices.filtered(lambda device: device.ip_address == TEST_IP)
        self.assertEqual(localized_device.country, 'France')

    def test_detection_usurpation_sid(self):
        session = self.authenticate(self.user_internal.login, self.user_internal.login)
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-user?readonly=0')

        self.hit('2024-01-01 08:00:00', '/test_http/greeting-user?readonly=0', headers={'session_id': session.sid}, ip=TEST_IP)
        devices, logs = self.get_devices_logs(self.user_internal)
        self.assertEqual(len(devices), 1)
        self.assertEqual(len(logs), 2)
        self.assertEqual(len(self.user_internal.device_ids), 1)

    def test_detection_devices_according_to_time_useragent(self):
        self.authenticate(self.user_admin.login, self.user_admin.login)
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome})
        self.assertEqual(len(self.user_admin.device_ids), 1)

        self.hit('2024-01-01 09:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome})
        self.assertEqual(len(self.user_admin.device_ids), 1)

        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_firefox})
        self.assertEqual(len(self.user_admin.device_ids), 2)

        self.hit('2024-01-01 09:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_firefox})
        self.assertEqual(len(self.user_admin.device_ids), 2)

    def test_detection_devices_according_to_user_or_admin(self):
        self.authenticate(self.user_admin.login, self.user_admin.login)
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0')
        self.hit('2024-01-01 09:00:00', '/test_http/greeting-public?readonly=0')
        self.authenticate(self.user_internal.login, self.user_internal.login)
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0')
        self.hit('2024-01-01 09:00:00', '/test_http/greeting-public?readonly=0')

        devices, logs = self.get_devices_logs()
        self.assertEqual(len(devices), 2)
        self.assertEqual(len(logs), 4)
        self.assertEqual(len(self.user_admin.device_ids), 1)
        self.assertEqual(len(self.user_internal.device_ids), 1)

        devices_from_admin = self.Device.with_user(self.user_admin).search([])
        devices_from_internal = self.Device.with_user(self.user_internal).search([])
        self.assertEqual(len(devices_from_admin), 2)
        self.assertEqual(len(devices_from_internal), 1)

    def test_differentiate_computer_and_mobile(self):
        self.authenticate(self.user_admin.login, self.user_admin.login)
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome})
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_android_chrome})

        devices, logs = self.get_devices_logs(self.user_admin)
        self.assertEqual(len(devices), 2)
        self.assertEqual(len(logs), 2)

        laptop_device = devices.filtered(lambda device: device.device_type == 'computer')
        mobile_device = devices.filtered(lambda device: device.device_type == 'mobile')
        self.assertEqual(len(laptop_device), 1)
        self.assertEqual(len(mobile_device), 1)

    def test_retrieve_linked_ip_addresses(self):
        self.authenticate(self.user_admin.login, self.user_admin.login)
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', ip='193.0.3.43')
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', ip='192.0.2.42')
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', ip='191.0.1.41')

        devices, _ = self.get_devices_logs(self.user_admin)
        self.assertEqual(len(devices), 1)
        self.assertIn('193.0.3.43', devices.linked_ip_addresses)
        self.assertIn('192.0.2.42', devices.linked_ip_addresses)
        self.assertIn('191.0.1.41', devices.linked_ip_addresses)

    def test_retrieve_linked_ip_addresses_according_to_devices(self):
        self.authenticate(self.user_admin.login, self.user_admin.login)
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome}, ip='193.0.3.43')
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome}, ip='192.0.2.42')
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_firefox}, ip='191.0.1.41')

        devices, _ = self.get_devices_logs(self.user_admin)
        self.assertEqual(len(devices), 2)
        device_chrome = devices.filtered(lambda device: device.browser == 'chrome')
        device_firefox = devices.filtered(lambda device: device.browser == 'firefox')
        self.assertIn('193.0.3.43', device_chrome.linked_ip_addresses)
        self.assertIn('192.0.2.42', device_chrome.linked_ip_addresses)
        self.assertNotIn('191.0.1.41', device_chrome.linked_ip_addresses)
        self.assertIn('191.0.1.41', device_firefox.linked_ip_addresses)

    def test_detection_no_trace_mechanism(self):
        session = self.authenticate(self.user_admin.login, self.user_admin.login)
        session._trace_disable = True
        odoo.http.root.session_store.save(session)
        res = self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0')
        self.assertEqual(res.status_code, 200)
        devices, logs = self.get_devices_logs(self.user_admin)
        self.assertEqual(len(devices), 0)
        self.assertEqual(len(logs), 0)

    def test_detection_device_default_order(self):
        self.authenticate(self.user_admin.login, self.user_admin.login)
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome})
        self.hit('2024-01-01 10:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_linux_firefox})
        self.hit('2024-01-01 09:00:00', '/test_http/greeting-public?readonly=0', headers={'User-Agent': USER_AGENT_android_chrome})
        devices, _ = self.get_devices_logs(self.user_admin)
        self.assertEqual(
            list(zip(devices.mapped('platform'), devices.mapped('browser'))),
            [('linux', 'firefox'), ('android', 'chrome'), ('linux', 'chrome')],
            "By default, devices should be found from the most recent to the least recent (according to their last activity)."
        )

    # --------------------
    # DELETION
    # --------------------

    def test_deletion_device(self):
        """
            A user is authenticated and the administrator
            wants to block his device (and therefore its session).
        """
        self.authenticate(self.user_internal.login, self.user_internal.login)
        res = self.hit('2024-01-01 08:00:00', '/test_http/greeting-user?readonly=0')
        self.assertNotIn('/web/login', res.url)

        user_internal_device = self.user_internal.device_ids
        self.assertEqual(len(user_internal_device), 1)
        self.assertEqual(user_internal_device.revoked, False)

        user_internal_device._revoke()

        res = self.hit('2024-01-01 08:00:01', '/test_http/greeting-user?readonly=0')
        self.assertIn('/web/login', res.url)

    def test_deletion_invalidate_sid(self):
        session = self.authenticate(self.user_internal.login, self.user_internal.login)
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-user?readonly=0')

        self.user_internal.device_ids._revoke()

        res = self.hit('2024-01-01 08:00:00', '/test_http/greeting-user?readonly=0', headers={'session_id': session.sid})
        self.assertIn('/web/login', res.url)

    def test_deletion_specific_device(self):
        self.authenticate(self.user_admin.login, self.user_admin.login)
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-user?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome})
        self.hit('2024-01-01 09:00:00', '/test_http/greeting-user?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome})
        self.authenticate(self.user_admin.login, self.user_admin.login)
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-user?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome})
        self.hit('2024-01-01 09:00:00', '/test_http/greeting-user?readonly=0', headers={'User-Agent': USER_AGENT_linux_chrome})
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-user?readonly=0', headers={'User-Agent': USER_AGENT_linux_firefox})

        devices, logs = self.get_devices_logs(self.user_admin)
        self.assertEqual(len(devices), 3)
        self.assertEqual(len(logs), 5)
        self.assertEqual(len(self.user_admin.device_ids), 3)

        self.user_admin.device_ids.filtered(lambda device: 'firefox' in device.browser)._revoke()

        res = self.hit('2024-01-01 08:00:30', '/test_http/greeting-user?readonly=0', headers={'User-Agent': USER_AGENT_linux_firefox})
        self.assertIn('/web/login', res.url)

    # --------------------
    # SPECIFIC USE CASE
    # --------------------

    def test_specific_public_user_write(self):
        """
            A public user who hits a non-readonly route
            does not have to create a session file if there
            are no changes in the session itself.
        """
        session = self.authenticate(None, None)
        self.hit('2024-01-01 08:00:00', '/test_http/greeting-public?readonly=0')

        # As we don't have a uid in the session, we shouldn't go through
        # the session check and therefore we won't go through the device update.
        # `authenticate` method in the test is not the real method.
        # To check that we are not creating a session (by making it dirty),
        # we can check that there is no `_trace`.
        # This means that the device logic will not create a session file
        # (because we are not passing in the `_update_device` logic).
        self.assertFalse(session._trace)
