# --- BEGIN COPYRIGHT BLOCK ---
# Copyright (C) 2025 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
# See LICENSE for details.
# --- END COPYRIGHT BLOCK ---
#
import base64
import logging
import pytest
import os
import ldap
import re
import time
from lib389._constants import DEFAULT_SUFFIX
from lib389.topologies import topology_st as topo
from lib389.idm.group import Groups, Group
from lib389.idm.user import UserAccount, UserAccounts
from lib389.dirsrv_log import DirsrvAuditJSONLog

log = logging.getLogger(__name__)

DN = "uid=audit_json,ou=people," + DEFAULT_SUFFIX
MAIN_KEYS = [
    "gm_time",
    "local_time",
    "target_dn",
    "bind_dn",
    "client_ip",
    "server_ip",
    "conn_id",
    "op_id",
    "result",
]

@pytest.fixture
def setup_test(topo, request):
    """Configure log settings"""
    inst = topo.standalone
    inst.config.replace('nsslapd-auditlog-logbuffering', 'off')
    inst.config.replace('nsslapd-auditlog-logging-enabled', 'on')
    inst.config.replace('nsslapd-auditfaillog-logging-enabled', 'on')
    inst.config.replace('nsslapd-auditlog-log-format', 'json')
    inst.config.replace('nsslapd-auditlog-display-attrs', 'cn')
    inst.deleteAuditLogs()


def get_log_event(inst, dn, op):
    """Get a specific audit log event by target_dn and op type
    """
    time.sleep(1)  # give a little time to flush to disk

    audit_log = DirsrvAuditJSONLog(inst)
    log_lines = audit_log.readlines()
    for line in log_lines:
        if re.match(r'[ \t\n]', line):
            # Skip log title lines and newlines
            continue

        event = audit_log.parse_line(line)
        if 'header' in event:
            # Skip header line
            continue
        if event['target_dn'].lower() == dn.lower() and op in event:
            return event

    # Not found, must assert
    assert False


def test_audit_json_logging(topo, setup_test):
    """Test audit json logging is working

    :id: 837a89a3-9c5a-4f8f-90fd-27c6b32f13f6
    :setup: Standalone Instance
    :steps:
        1. Add entry
        2. Check it was logged in json and default keys are present
        3. Modify entry
        4. Check audit log
        5. Modrdn of entry
        6. Check log
        7. Delete entry
        8. Check log
        9. Modify time format
        10. Delete of non-existent entry fails
        11. Check log for expected "result == 32"
        12. Verify new time format is applied
    :expectedresults:
        1. Success
        2. Success
        3. Success
        4. Success
        5. Success
        6. Success
        7. Success
        8. Success
        9. Success
        10. Success
        11. Success
        12. Success
    """
    inst = topo.standalone

    # Add entry
    users = UserAccounts(inst, DEFAULT_SUFFIX)
    user_dn = "uid=test_audit_log,ou=people," + DEFAULT_SUFFIX
    user = users.ensure_state(properties={
        'uid': 'test_audit_log',
        'cn': 'test',
        'sn': 'user',
        'uidNumber': '1000',
        'gidNumber': '1000',
        'homeDirectory': '/home/test',
        'userPassword': 'pppppppp'
    })

    event = get_log_event(inst, user_dn, "add")
    # Check all the expected keys are present
    for key in MAIN_KEYS:
        assert key in event and event[key] != ""

    # Check specific values
    assert event['result'] == 0
    assert event['add']
    assert 'id_list' in event
    assert event['id_list'][0]['cn'] == "test"

    # Modify entry
    user.replace('sn', 'new sn')
    event = get_log_event(inst, user_dn, "modify")
    assert event['result'] == 0
    assert event['modify'][0]['op'] == "replace"
    assert event['modify'][0]['attr'] == "sn"
    assert event['modify'][0]['values'][0] == "new sn"

    # ModRDN entry
    NEW_SUP = "ou=groups," + DEFAULT_SUFFIX
    user.rename('uid=test_modrdn', newsuperior=NEW_SUP, deloldrdn=True)
    event = get_log_event(inst, user_dn, "modrdn")
    assert event['result'] == 0
    assert event['modrdn']['newrdn'] == "uid=test_modrdn"
    assert event['modrdn']['deleteoldrdn'] is True
    assert event['modrdn']['newsuperior'] == NEW_SUP

    # Delete entry
    DN = "uid=test_modrdn," + NEW_SUP
    user.delete()
    event = get_log_event(inst, DN, "delete")
    assert event['result'] == 0
    assert event['delete']['dn'] == DN

    # CHange time format and check it after the next test
    inst.config.replace('nsslapd-auditlog-time-format', '%F %T')

    # Perform invalid update (result != 0)
    DN = "uid=does_not_exist," + DEFAULT_SUFFIX
    user = UserAccount(inst, DN)
    with pytest.raises(ldap.NO_SUCH_OBJECT):
        user.delete()
    event = get_log_event(inst, DN, "delete")
    assert event['result'] == 32

    # Check time format
    assert re.match(
        r'[0-9]{4}\-[0-9]{2}\-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}',
        event['local_time'])


def test_audit_json_logging_with_binary_value(topo, setup_test):
    """Test audit json logging is working with binary values

    :id: 79870a11-907d-490c-8d9d-3ca242902966
    :setup: Standalone Instance
    :steps:
        1. Get audit log event
        2. Check nsState attribute is present
        3. Check modifiersname attribute is present
        4. Check nsState value is base64 encoded
        5. Check modifiersname value is not base64 encoded
        6. Add large binary value is correctly encoded
    :expectedresults:
        1. Success
        2. Success
        3. Success
        4. Success
        5. Success
        6. Success
    """
    inst = topo.standalone

    event = get_log_event(inst, "cn=uniqueid generator,cn=config", "modify")
    assert event['result'] == 0
    assert event['modify'][0]['op'] == "replace"
    assert event['modify'][0]['attr'] == "nsState"
    assert event['modify'][1]['attr'] == "modifiersname"

    # Decoding nsState should not raise an error
    nsState_value = event['modify'][0]['values_encoded'][0]
    assert base64.b64decode(nsState_value, validate=True)

    # Decoding modifiersname should raise an error
    modifersname_value = event['modify'][1]['values'][0]
    with pytest.raises(base64.binascii.Error):
        base64.b64decode(modifersname_value, validate=True)

    # Add large binary value and mixed value
    groups = Groups(inst, DEFAULT_SUFFIX, rdn=None)
    group_properties = {
        'cn': 'binary_group',
        'description': 'testgroup for binary value',
        'objectclass': ['nsValueItem', 'top', 'groupOfNames']}
    test_group = groups.create(properties=group_properties)

    # Add large binary value
    bin_value_large = bytes([(i % 256) for i in range(2 * 512 * 512)])
    test_group.add('nsValueBin', bin_value_large)
    event = get_log_event(inst, test_group.dn, "modify")
    assert event['result'] == 0
    assert event['modify'][0]['op'] == "add"
    assert event['modify'][0]['attr'] == "nsValueBin"
    bin_value = event['modify'][0]['values_encoded'][0]
    assert base64.b64decode(bin_value, validate=True)


if __name__ == '__main__':
    # Run isolated
    # -s for DEBUG mode
    CURRENT_FILE = os.path.realpath(__file__)
    pytest.main(["-s", CURRENT_FILE])
