/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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
 *
 *        http://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.
 */
package org.apache.causeway.extensions.audittrail.applib.integtests;

import java.util.stream.Collectors;

import jakarta.inject.Inject;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

import org.apache.causeway.applib.mixins.system.DomainChangeRecord;
import org.apache.causeway.applib.services.bookmark.BookmarkService;
import org.apache.causeway.applib.services.iactnlayer.InteractionService;
import org.apache.causeway.applib.services.wrapper.WrapperFactory;
import org.apache.causeway.core.config.presets.CausewayPresets;
import org.apache.causeway.extensions.audittrail.applib.dom.AuditTrailEntry;
import org.apache.causeway.extensions.audittrail.applib.dom.AuditTrailEntryRepository;
import org.apache.causeway.extensions.audittrail.applib.integtests.model.Counter;
import org.apache.causeway.extensions.audittrail.applib.integtests.model.CounterRepository;
import org.apache.causeway.extensions.audittrail.applib.integtests.model.Counter_bumpUsingMixin;
import org.apache.causeway.testing.integtestsupport.applib.CausewayIntegrationTestAbstract;

public abstract class AuditTrail_IntegTestAbstract extends CausewayIntegrationTestAbstract {

    @BeforeAll
    static void beforeAll() {
        CausewayPresets.forcePrototyping();
    }

    @BeforeEach
    void setUp() {
        counterRepository.removeAll();
        interactionService.nextInteraction();

        auditTrailEntryRepository.removeAll();
        interactionService.nextInteraction();

        assertThat(counterRepository.find()).isEmpty();
        assertThat(auditTrailEntryRepository.findAll()).isEmpty();
    }

    protected abstract Counter newCounter(String name);

    @Test
    void created() {

        // when
        var counter1 = counterRepository.persist(newCounter("counter-1"));
        var target1 = bookmarkService.bookmarkFor(counter1).orElseThrow();
        interactionService.nextInteraction();

        // then
        var entries = auditTrailEntryRepository.findAll();
        var propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList());
        assertThat(propertyIds).contains("name", "num", "num2");

        var entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x));
        assertThat(entriesById.get("name"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#name"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("[NEW]"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("counter-1"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getInteractionId).isNotNull())
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getSequence).isEqualTo(0))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTarget).isEqualTo(target1))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTimestamp).isNotNull())
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getType).isEqualTo(DomainChangeRecord.ChangeType.AUDIT_ENTRY))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getUsername).isEqualTo("__system"));
        assertThat(entriesById.get("num"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("[NEW]"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isNull());
        assertThat(entriesById.get("num2"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num2"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("[NEW]"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isNull());
    }

    @Test
    void updated_using_mixin() {

        // given
        var counter1 = counterRepository.persist(newCounter("counter-1"));
        var target1 = bookmarkService.bookmarkFor(counter1).orElseThrow();
        interactionService.nextInteraction();

        auditTrailEntryRepository.removeAll();
        interactionService.nextInteraction();

        assertThat(counterRepository.find()).hasSize(1);
        assertThat(auditTrailEntryRepository.findAll()).isEmpty();

        // when
        counter1 = bookmarkService.lookup(target1, Counter.class).orElseThrow();
        wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act();
        interactionService.nextInteraction();

        // then
        var entries = auditTrailEntryRepository.findAll();
        var propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList());
        assertThat(propertyIds).containsExactly("num");

        var entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x));
        assertThat(entriesById.get("num"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isNull())
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("1"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getInteractionId).isNotNull())
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getSequence).isEqualTo(0))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTarget).isEqualTo(target1))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTimestamp).isNotNull())
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getType).isEqualTo(DomainChangeRecord.ChangeType.AUDIT_ENTRY))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getUsername).isEqualTo("__system"));

        // given
        auditTrailEntryRepository.removeAll();
        interactionService.nextInteraction();

        // when bump again
        counter1 = bookmarkService.lookup(target1, Counter.class).orElseThrow();
        wrapperFactory.wrapMixin(Counter_bumpUsingMixin.class, counter1).act();
        interactionService.nextInteraction();

        // then
        entries = auditTrailEntryRepository.findAll();
        propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList());
        assertThat(propertyIds).containsExactly("num");

        entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x));
        assertThat(entriesById.get("num"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("1"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("2"));

    }

    @Test
    void deleted() {

        // given
        var counter1 = counterRepository.persist(newCounter("counter-1"));
        counter1.setNum(1L);
        counter1.setNum2(2L);
        var target1 = bookmarkService.bookmarkFor(counter1).orElseThrow();
        interactionService.nextInteraction();

        auditTrailEntryRepository.removeAll();
        interactionService.nextInteraction();

        // when
        counter1 = bookmarkService.lookup(target1, Counter.class).orElseThrow();
        counterRepository.remove(counter1);
        interactionService.nextInteraction();

        // then
        var entries = auditTrailEntryRepository.findAll();
        var propertyIds = entries.stream().map(AuditTrailEntry::getPropertyId).collect(Collectors.toList());
        assertThat(propertyIds).contains("name", "num", "num2");

        var entriesById = entries.stream().collect(Collectors.toMap(AuditTrailEntry::getPropertyId, x -> x));
        assertThat(entriesById.get("name"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#name"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("counter-1"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("[DELETED]"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getInteractionId).isNotNull())
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getSequence).isEqualTo(0))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTarget).isEqualTo(target1))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getTimestamp).isNotNull())
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getType).isEqualTo(DomainChangeRecord.ChangeType.AUDIT_ENTRY))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getUsername).isEqualTo("__system"));
        assertThat(entriesById.get("num"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("1"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("[DELETED]"));
        assertThat(entriesById.get("num2"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getLogicalMemberIdentifier).isEqualTo("audittrail.test.Counter#num2"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPreValue).isEqualTo("2"))
                .satisfies(e -> assertThat(e).extracting(AuditTrailEntry::getPostValue).isEqualTo("[DELETED]"));

    }

    @Inject InteractionService interactionService;
    @Inject CounterRepository<? extends Counter> counterRepository;
    @Inject WrapperFactory wrapperFactory;
    @Inject BookmarkService bookmarkService;
    @Inject AuditTrailEntryRepository auditTrailEntryRepository;

}
