// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.sync;

import android.util.Pair;

import androidx.test.filters.LargeTest;

import org.hamcrest.Matchers;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.CriteriaNotSatisfiedException;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.util.browser.sync.SyncTestUtil;
import org.chromium.components.sync.ModelType;
import org.chromium.components.sync.UserSelectableType;
import org.chromium.components.sync.protocol.AutofillProfileSpecifics;
import org.chromium.components.sync.protocol.EntitySpecifics;

import java.util.ArrayList;
import java.util.List;

/**
 * Test suite for the autofill profile sync data type.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class AutofillTest {
    @Rule
    public SyncTestRule mSyncTestRule = new SyncTestRule();

    private static final String AUTOFILL_TYPE = "Autofill Profiles";

    private static final String GUID = "EDC609ED-7EEE-4F27-B00C-423242A9C44B";

    private static final String STREET = "1600 Amphitheatre Pkwy";
    private static final String CITY = "Mountain View";
    private static final String MODIFIED_CITY = "Sunnyvale";
    private static final String STATE = "CA";
    private static final String ZIP = "94043";

    // A container to store autofill profile information for data verification.
    private static class Autofill {
        public final String id;
        public final String clientTagHash;
        public final String street;
        public final String city;
        public final String state;
        public final String zip;

        public Autofill(String id, String clientTagHash, String street, String city, String state,
                String zip) {
            this.id = id;
            this.clientTagHash = clientTagHash;
            this.street = street;
            this.city = city;
            this.state = state;
            this.zip = zip;
        }
    }

    @Before
    public void setUp() throws Exception {
        mSyncTestRule.setUpAccountAndEnableSyncForTesting();
        // Make sure the initial state is clean.
        assertClientAutofillProfileCount(0);
        assertServerAutofillProfileCountWithName(0, STREET);
    }

    // Test syncing an autofill profile from server to client.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testDownloadAutofill() throws Exception {
        addServerAutofillProfile(getServerAutofillProfile(STREET, CITY, STATE, ZIP));
        SyncTestUtil.triggerSync();
        waitForClientAutofillProfileCount(1);

        // Verify data synced to client.
        List<Autofill> autofills = getClientAutofillProfiles();
        Assert.assertEquals(
                "Only the injected autofill should exist on the client.", 1, autofills.size());
        Autofill autofill = autofills.get(0);
        Assert.assertEquals("The wrong street was found for the address.", STREET, autofill.street);
        Assert.assertEquals("The wrong city was found for the autofill.", CITY, autofill.city);
        Assert.assertEquals("The wrong state was found for the autofill.", STATE, autofill.state);
        Assert.assertEquals("The wrong zip was found for the autofill.", ZIP, autofill.zip);
    }

    // Test syncing an autofill profile modification from server to client.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testDownloadAutofillModification() throws Exception {
        // Add the entity to test modifying.
        addServerAutofillProfile(getServerAutofillProfile(STREET, CITY, STATE, ZIP));
        SyncTestUtil.triggerSync();
        waitForClientAutofillProfileCount(1);

        // Modify on server, sync, and verify modification locally.
        Autofill autofill = getClientAutofillProfiles().get(0);
        mSyncTestRule.getFakeServerHelper().modifyEntitySpecifics(
                autofill.id, getServerAutofillProfile(STREET, MODIFIED_CITY, STATE, ZIP));
        SyncTestUtil.triggerSync();
        mSyncTestRule.pollInstrumentationThread(() -> {
            try {
                Autofill modifiedAutofill = getClientAutofillProfiles().get(0);
                Criteria.checkThat(modifiedAutofill.city, Matchers.is(MODIFIED_CITY));
            } catch (JSONException ex) {
                throw new RuntimeException(ex);
            }
        });
    }

    // Test syncing an autofill profile deletion from server to client.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testDownloadDeletedAutofill() throws Exception {
        // Add the entity to test deleting.
        addServerAutofillProfile(getServerAutofillProfile(STREET, CITY, STATE, ZIP));
        SyncTestUtil.triggerSync();
        waitForClientAutofillProfileCount(1);

        // Delete on server, sync, and verify deleted locally.
        Autofill autofill = getClientAutofillProfiles().get(0);
        mSyncTestRule.getFakeServerHelper().deleteEntity(autofill.id, autofill.clientTagHash);
        SyncTestUtil.triggerSync();
        waitForClientAutofillProfileCount(0);
    }

    // Test that autofill profiles don't get synced if the data type is disabled.
    @Test
    @LargeTest
    @Feature({"Sync"})
    public void testDisabledNoDownloadAutofill() throws Exception {
        // The AUTOFILL type here controls both AUTOFILL and AUTOFILL_PROFILE.
        mSyncTestRule.disableDataType(UserSelectableType.AUTOFILL);
        addServerAutofillProfile(getServerAutofillProfile(STREET, CITY, STATE, ZIP));
        SyncTestUtil.triggerSyncAndWaitForCompletion();
        assertClientAutofillProfileCount(0);
    }

    private EntitySpecifics getServerAutofillProfile(
            String street, String city, String state, String zip) {
        AutofillProfileSpecifics profile = AutofillProfileSpecifics.newBuilder()
                                                   .setGuid(GUID)
                                                   .setAddressHomeLine1(street)
                                                   .setAddressHomeCity(city)
                                                   .setAddressHomeState(state)
                                                   .setAddressHomeZip(zip)
                                                   .build();
        return EntitySpecifics.newBuilder().setAutofillProfile(profile).build();
    }

    private void addServerAutofillProfile(EntitySpecifics specifics) {
        mSyncTestRule.getFakeServerHelper().injectUniqueClientEntity(
                specifics.getAutofillProfile().getGuid() /* nonUniqueName */,
                specifics.getAutofillProfile().getGuid() /* clientTag */, specifics);
    }

    private List<Autofill> getClientAutofillProfiles() throws JSONException {
        List<Pair<String, JSONObject>> entities =
                SyncTestUtil.getLocalData(mSyncTestRule.getTargetContext(), AUTOFILL_TYPE);
        List<Autofill> autofills = new ArrayList<Autofill>(entities.size());
        for (Pair<String, JSONObject> entity : entities) {
            String id = entity.first;
            JSONObject profile = entity.second;
            String clientTagHash = "";
            if (profile.has("metadata")) {
                JSONObject metadata = profile.getJSONObject("metadata");
                if (metadata.has("client_tag_hash")) {
                    clientTagHash = metadata.getString("client_tag_hash");
                }
            }
            String street = profile.getString("address_home_line1");
            String city = profile.getString("address_home_city");
            String state = profile.getString("address_home_state");
            String zip = profile.getString("address_home_zip");
            autofills.add(new Autofill(id, clientTagHash, street, city, state, zip));
        }
        return autofills;
    }

    private void assertClientAutofillProfileCount(int count) throws JSONException {
        Assert.assertEquals("There should be " + count + " local autofill profiles.", count,
                SyncTestUtil.getLocalData(mSyncTestRule.getTargetContext(), AUTOFILL_TYPE).size());
    }

    private void assertServerAutofillProfileCountWithName(int count, String name) {
        Assert.assertTrue("Expected " + count + " server autofill profiles with name " + name + ".",
                mSyncTestRule.getFakeServerHelper().verifyEntityCountByTypeAndName(
                        count, ModelType.AUTOFILL_PROFILE, name));
    }

    private void waitForClientAutofillProfileCount(int count) {
        CriteriaHelper.pollInstrumentationThread(() -> {
            try {
                Criteria.checkThat(
                        SyncTestUtil.getLocalData(mSyncTestRule.getTargetContext(), AUTOFILL_TYPE)
                                .size(),
                        Matchers.is(count));
            } catch (JSONException ex) {
                throw new CriteriaNotSatisfiedException(ex);
            }
        }, SyncTestUtil.TIMEOUT_MS, SyncTestUtil.INTERVAL_MS);
    }
}
