/****************************************************************
 * 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.james.webadmin.integration.memory;

import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT;
import static org.apache.james.jmap.JMAPTestingConstants.ALICE;
import static org.apache.james.jmap.JMAPTestingConstants.ALICE_PASSWORD;
import static org.apache.james.jmap.JMAPTestingConstants.BOB;
import static org.apache.james.jmap.JMAPTestingConstants.BOB_PASSWORD;
import static org.apache.james.jmap.JMAPTestingConstants.CEDRIC;
import static org.apache.james.jmap.JMAPTestingConstants.CEDRIC_PASSWORD;
import static org.apache.james.jmap.JMAPTestingConstants.DOMAIN;
import static org.apache.james.jmap.JMAPTestingConstants.jmapRequestSpecBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.SoftAssertions.assertSoftly;
import static org.awaitility.Durations.TEN_SECONDS;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.hasSize;

import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;

import javax.inject.Inject;

import org.apache.http.HttpStatus;
import org.apache.james.GuiceJamesServer;
import org.apache.james.JamesServerBuilder;
import org.apache.james.JamesServerExtension;
import org.apache.james.MemoryJamesConfiguration;
import org.apache.james.MemoryJamesServerMain;
import org.apache.james.core.Username;
import org.apache.james.core.quota.QuotaCountLimit;
import org.apache.james.core.quota.QuotaCountUsage;
import org.apache.james.core.quota.QuotaSizeLimit;
import org.apache.james.core.quota.QuotaSizeUsage;
import org.apache.james.jmap.api.filtering.FilteringManagement;
import org.apache.james.jmap.api.filtering.Rule;
import org.apache.james.jmap.api.filtering.Rules;
import org.apache.james.jmap.api.filtering.Version;
import org.apache.james.jmap.draft.JmapGuiceProbe;
import org.apache.james.mailbox.MessageManager;
import org.apache.james.mailbox.model.MailboxACL;
import org.apache.james.mailbox.model.MailboxPath;
import org.apache.james.mailbox.model.Quota;
import org.apache.james.mailbox.model.QuotaRoot;
import org.apache.james.mime4j.dom.Message;
import org.apache.james.modules.ACLProbeImpl;
import org.apache.james.modules.MailboxProbeImpl;
import org.apache.james.modules.QuotaProbesImpl;
import org.apache.james.modules.TestJMAPServerModule;
import org.apache.james.probe.DataProbe;
import org.apache.james.util.Port;
import org.apache.james.utils.DataProbeImpl;
import org.apache.james.utils.GuiceProbe;
import org.apache.james.utils.WebAdminGuiceProbe;
import org.apache.james.webadmin.WebAdminUtils;
import org.awaitility.Awaitility;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.github.fge.lambdas.Throwing;
import com.google.inject.multibindings.Multibinder;

import io.restassured.RestAssured;
import io.restassured.specification.RequestSpecification;
import reactor.core.publisher.Mono;

class MemoryUsernameChangeIntegrationTest {
    public static final class FilterProbe implements GuiceProbe {
        private final FilteringManagement filteringManagement;

        @Inject
        public FilterProbe(FilteringManagement filteringManagement) {
            this.filteringManagement = filteringManagement;
        }

        public void defineRulesForUser(Username username, List<Rule> rules, Optional<Version> ifInState) {
            Mono.from(filteringManagement.defineRulesForUser(username, rules, ifInState))
                .block();
        }

        public Rules listRulesForUser(Username username) {
            return Mono.from(filteringManagement.listRulesForUser(username))
                .block();
        }
    }

    private static final String NAME = "a name";
    private static final Rule.Condition CONDITION = Rule.Condition.of(Rule.Condition.Field.CC, Rule.Condition.Comparator.CONTAINS, "something");
    private static final Rule.Action ACTION = Rule.Action.of(Rule.Action.AppendInMailboxes.withMailboxIds("id-01"));
    private static final Rule.Builder RULE_BUILDER = Rule.builder().name(NAME).condition(CONDITION).action(ACTION);
    private static final Rule RULE_1 = RULE_BUILDER.id(Rule.Id.of("1")).build();
    private static final Rule RULE_2 = RULE_BUILDER.id(Rule.Id.of("2")).build();
    private static final Optional<Version> NO_VERSION = Optional.empty();

    @RegisterExtension
    static JamesServerExtension jamesServerExtension = new JamesServerBuilder<MemoryJamesConfiguration>(tmpDir ->
        MemoryJamesConfiguration.builder()
            .workingDirectory(tmpDir)
            .configurationFromClasspath()
            .usersRepository(DEFAULT)
            .build())
        .server(configuration -> MemoryJamesServerMain.createServer(configuration)
            .overrideWith(binder -> Multibinder.newSetBinder(binder, GuiceProbe.class)
                .addBinding().to(FilterProbe.class))
            .overrideWith(new TestJMAPServerModule()))
        .build();

    private RequestSpecification webAdminApi;

    @BeforeEach
    void setUp(GuiceJamesServer jmapServer) throws Exception {
        DataProbe dataProbe = jmapServer.getProbe(DataProbeImpl.class);
        dataProbe.addDomain(DOMAIN);
        dataProbe.addUser(BOB.asString(), BOB_PASSWORD);
        dataProbe.addUser(ALICE.asString(), ALICE_PASSWORD);
        dataProbe.addUser(CEDRIC.asString(), CEDRIC_PASSWORD);

        Port jmapPort = jmapServer.getProbe(JmapGuiceProbe.class).getJmapPort();
        RestAssured.requestSpecification = jmapRequestSpecBuilder
            .setPort(jmapPort.getValue())
            .build();

        webAdminApi = WebAdminUtils.spec(jmapServer.getProbe(WebAdminGuiceProbe.class).getWebAdminPort());
    }

    @Test
    void shouldMigrateACLs(GuiceJamesServer server) throws Exception {
        server.getProbe(MailboxProbeImpl.class).createMailbox(MailboxPath.inbox(CEDRIC));
        server.getProbe(ACLProbeImpl.class).addRights(MailboxPath.inbox(CEDRIC), ALICE.asString(), MailboxACL.FULL_RIGHTS);

        String taskId = webAdminApi
            .queryParam("action", "rename")
            .post("/users/" + ALICE.asString() + "/rename/" + BOB.asString())
            .jsonPath()
            .get("taskId");

        webAdminApi.get("/tasks/" + taskId + "/await");

        MailboxACL acls = server.getProbe(ACLProbeImpl.class).retrieveRights(MailboxPath.inbox(CEDRIC));

        assertThat(acls.getEntries()).hasSize(2)
            .containsEntry(MailboxACL.EntryKey.createUserEntryKey(BOB), MailboxACL.FULL_RIGHTS);
    }

    @Test
    void shouldMigrateMailboxes() {
        webAdminApi.put("/users/" + ALICE.asString() + "/mailboxes/test");

        String taskId = webAdminApi
            .queryParam("action", "rename")
            .post("/users/" + ALICE.asString() + "/rename/" + BOB.asString())
            .jsonPath()
            .get("taskId");

        webAdminApi.get("/tasks/" + taskId + "/await");

        webAdminApi.get("/users/" + ALICE.asString() + "/mailboxes")
            .then()
            .body(".", hasSize(0));

        webAdminApi.get("/users/" + BOB.asString() + "/mailboxes")
            .then()
            .body(".", hasSize(1))
            .body("[0].mailboxName", is("test"));
    }

    @Test
    void shouldAdaptForwards() {
        String taskId = webAdminApi
            .queryParam("action", "rename")
            .post("/users/" + ALICE.asString() + "/rename/" + BOB.asString())
            .jsonPath()
            .get("taskId");

        webAdminApi.get("/tasks/" + taskId + "/await");

        webAdminApi.get("/address/forwards/" + BOB.asString())
            .then()
            .statusCode(404);

        webAdminApi.get("/address/forwards/" + ALICE.asString())
            .then()
            .body(".", hasSize(1))
            .body("[0].mailAddress", is(BOB.asString()));
    }

    @Test
    void shouldAdaptDelegation() {
        webAdminApi.put("/users/" + ALICE.asString() + "/authorizedUsers/" + CEDRIC.asString());

        String taskId = webAdminApi
            .queryParam("action", "rename")
            .post("/users/" + ALICE.asString() + "/rename/" + BOB.asString())
            .jsonPath()
            .get("taskId");

        webAdminApi.get("/tasks/" + taskId + "/await");

        webAdminApi.get("/users/" + ALICE.asString() + "/authorizedUsers")
            .then()
                .body(".", hasSize(0));
        webAdminApi.get("/users/" + BOB.asString() + "/authorizedUsers")
            .then()
                .body(".", hasSize(1))
                .body("[0]", is(CEDRIC.asString()));
    }

    @Test
    void shouldAdaptFilters(GuiceJamesServer server) {
        FilterProbe filterProbe = server.getProbe(FilterProbe.class);
        filterProbe.defineRulesForUser(ALICE, List.of(RULE_1), NO_VERSION);

        String taskId = webAdminApi
            .queryParam("action", "rename")
            .post("/users/" + ALICE.asString() + "/rename/" + BOB.asString())
            .jsonPath()
            .get("taskId");

        webAdminApi.get("/tasks/" + taskId + "/await");

        assertThat(filterProbe.listRulesForUser(BOB).getRules())
            .containsOnly(RULE_1);
        assertThat(filterProbe.listRulesForUser(ALICE).getRules())
            .isEmpty();
    }

    @Test
    void shouldAdaptQuotas(GuiceJamesServer server) throws Exception {
        server.getProbe(MailboxProbeImpl.class).createMailbox(MailboxPath.inbox(BOB));

        QuotaProbesImpl quotaProbes = server.getProbe(QuotaProbesImpl.class);
        QuotaRoot bobQuotaRoot = quotaProbes.getQuotaRoot(MailboxPath.inbox(BOB));

        quotaProbes.setMaxMessageCount(bobQuotaRoot, QuotaCountLimit.count(50));
        quotaProbes.setMaxStorage(bobQuotaRoot, QuotaSizeLimit.size(1000));

        server.getProbe(MailboxProbeImpl.class)
            .appendMessage(BOB.asString(), MailboxPath.inbox(BOB),
                MessageManager.AppendCommand.from(Message.Builder.of()
                    .setSubject("test")
                    .setBody("testmail", StandardCharsets.UTF_8)
                    .build()));

        Awaitility.await()
            .atMost(TEN_SECONDS)
            .untilAsserted(() -> {
                assertThat(quotaProbes.getMessageCountQuota(bobQuotaRoot)
                    .getUsed().asLong())
                    .isEqualTo(1L);
                assertThat(quotaProbes.getStorageQuota(bobQuotaRoot)
                    .getUsed().asLong())
                    .isEqualTo(85L);
            });

        String taskId = webAdminApi
            .queryParam("action", "rename")
            .post("/users/" + BOB.asString() + "/rename/" + ALICE.asString())
            .jsonPath()
            .get("taskId");

        webAdminApi.get("/tasks/" + taskId + "/await")
            .then()
            .statusCode(HttpStatus.SC_OK)
            .body("additionalInformation.status.QuotaUsernameChangeTaskStep", Matchers.is("DONE"));

        QuotaRoot aliceQuotaRoot = quotaProbes.getQuotaRoot(MailboxPath.inbox(ALICE));

        assertSoftly(softly -> {
            softly.assertThat(Throwing.supplier(() -> quotaProbes.getMessageCountQuota(aliceQuotaRoot)).get())
                .isEqualTo(Quota.<QuotaCountLimit, QuotaCountUsage>builder().used(QuotaCountUsage.count(1)).computedLimit(QuotaCountLimit.count(50)).build());
            softly.assertThat(Throwing.supplier(() -> quotaProbes.getStorageQuota(aliceQuotaRoot)).get())
                .isEqualTo(Quota.<QuotaSizeLimit, QuotaSizeUsage>builder().used(QuotaSizeUsage.size(85)).computedLimit(QuotaSizeLimit.size(1000)).build());
        });
    }
}
