/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed 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 com.android.tv.data;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.tv.data.api.Channel;
import com.android.tv.testing.ComparatorTester;
import com.android.tv.util.TvInputManagerHelper;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import java.util.Comparator;

/** Tests for {@link ChannelImpl}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
public class ChannelImplTest {
    // Used for testing TV inputs with invalid input package. This could happen when a TV input is
    // uninstalled while drawing an app link card.
    private static final String INVALID_TV_INPUT_PACKAGE_NAME = "com.android.tv.invalid_tv_input";
    // Used for testing TV inputs defined inside of TV app.
    private static final String LIVE_CHANNELS_PACKAGE_NAME = "com.android.tv";
    // Used for testing a TV input which doesn't have its leanback launcher activity.
    private static final String NONE_LEANBACK_TV_INPUT_PACKAGE_NAME =
            "com.android.tv.none_leanback_tv_input";
    // Used for testing a TV input which has its leanback launcher activity.
    private static final String LEANBACK_TV_INPUT_PACKAGE_NAME = "com.android.tv.leanback_tv_input";
    private static final String TEST_APP_LINK_TEXT = "test_app_link_text";
    private static final String PARTNER_INPUT_ID = "partner";
    private static final ActivityInfo TEST_ACTIVITY_INFO = new ActivityInfo();

    private Context mMockContext;
    private Intent mInvalidIntent;
    private Intent mValidIntent;

    @Before
    public void setUp() throws NameNotFoundException {
        mInvalidIntent = new Intent(Intent.ACTION_VIEW);
        mInvalidIntent.setComponent(new ComponentName(INVALID_TV_INPUT_PACKAGE_NAME, ".test"));
        mValidIntent = new Intent(Intent.ACTION_VIEW);
        mValidIntent.setComponent(new ComponentName(LEANBACK_TV_INPUT_PACKAGE_NAME, ".test"));
        Intent liveChannelsIntent = new Intent(Intent.ACTION_VIEW);
        liveChannelsIntent.setComponent(
                new ComponentName(LIVE_CHANNELS_PACKAGE_NAME, ".MainActivity"));
        Intent leanbackTvInputIntent = new Intent(Intent.ACTION_VIEW);
        leanbackTvInputIntent.setComponent(
                new ComponentName(LEANBACK_TV_INPUT_PACKAGE_NAME, ".test"));

        PackageManager mockPackageManager = Mockito.mock(PackageManager.class);
        Mockito.when(
                        mockPackageManager.getLeanbackLaunchIntentForPackage(
                                INVALID_TV_INPUT_PACKAGE_NAME))
                .thenReturn(null);
        Mockito.when(
                        mockPackageManager.getLeanbackLaunchIntentForPackage(
                                LIVE_CHANNELS_PACKAGE_NAME))
                .thenReturn(liveChannelsIntent);
        Mockito.when(
                        mockPackageManager.getLeanbackLaunchIntentForPackage(
                                NONE_LEANBACK_TV_INPUT_PACKAGE_NAME))
                .thenReturn(null);
        Mockito.when(
                        mockPackageManager.getLeanbackLaunchIntentForPackage(
                                LEANBACK_TV_INPUT_PACKAGE_NAME))
                .thenReturn(leanbackTvInputIntent);

        // Channel.getAppLinkIntent() calls initAppLinkTypeAndIntent() which calls
        // Intent.resolveActivityInfo() which calls PackageManager.getActivityInfo().
        Mockito.doAnswer(
                        new Answer<ActivityInfo>() {
                            @Override
                            public ActivityInfo answer(InvocationOnMock invocation) {
                                // We only check the package name, since the class name can be
                                // changed
                                // when an intent is changed to an uri and created from the uri.
                                // (ex, ".className" -> "packageName.className")
                                return mValidIntent
                                                .getComponent()
                                                .getPackageName()
                                                .equals(
                                                        ((ComponentName)
                                                                        invocation
                                                                                .getArguments()[0])
                                                                .getPackageName())
                                        ? TEST_ACTIVITY_INFO
                                        : null;
                            }
                        })
                .when(mockPackageManager)
                .getActivityInfo(ArgumentMatchers.<ComponentName>any(), ArgumentMatchers.anyInt());

        mMockContext = Mockito.mock(Context.class);
        Mockito.when(mMockContext.getApplicationContext()).thenReturn(mMockContext);
        Mockito.when(mMockContext.getPackageName()).thenReturn(LIVE_CHANNELS_PACKAGE_NAME);
        Mockito.when(mMockContext.getPackageManager()).thenReturn(mockPackageManager);
    }

    @Test
    public void testGetAppLinkType_NoText_NoIntent() {
        assertAppLinkType(Channel.APP_LINK_TYPE_NONE, INVALID_TV_INPUT_PACKAGE_NAME, null, null);
        assertAppLinkType(Channel.APP_LINK_TYPE_NONE, LIVE_CHANNELS_PACKAGE_NAME, null, null);
        assertAppLinkType(
                Channel.APP_LINK_TYPE_NONE, NONE_LEANBACK_TV_INPUT_PACKAGE_NAME, null, null);
        assertAppLinkType(Channel.APP_LINK_TYPE_APP, LEANBACK_TV_INPUT_PACKAGE_NAME, null, null);
    }

    @Test
    public void testGetAppLinkType_NoText_InvalidIntent() {
        assertAppLinkType(
                Channel.APP_LINK_TYPE_NONE, INVALID_TV_INPUT_PACKAGE_NAME, null, mInvalidIntent);
        assertAppLinkType(
                Channel.APP_LINK_TYPE_NONE, LIVE_CHANNELS_PACKAGE_NAME, null, mInvalidIntent);
        assertAppLinkType(
                Channel.APP_LINK_TYPE_NONE,
                NONE_LEANBACK_TV_INPUT_PACKAGE_NAME,
                null,
                mInvalidIntent);
        assertAppLinkType(
                Channel.APP_LINK_TYPE_APP, LEANBACK_TV_INPUT_PACKAGE_NAME, null, mInvalidIntent);
    }

    @Test
    public void testGetAppLinkType_NoText_ValidIntent() {
        assertAppLinkType(
                Channel.APP_LINK_TYPE_NONE, INVALID_TV_INPUT_PACKAGE_NAME, null, mValidIntent);
        assertAppLinkType(
                Channel.APP_LINK_TYPE_NONE, LIVE_CHANNELS_PACKAGE_NAME, null, mValidIntent);
        assertAppLinkType(
                Channel.APP_LINK_TYPE_NONE,
                NONE_LEANBACK_TV_INPUT_PACKAGE_NAME,
                null,
                mValidIntent);
        assertAppLinkType(
                Channel.APP_LINK_TYPE_APP, LEANBACK_TV_INPUT_PACKAGE_NAME, null, mValidIntent);
    }

    @Test
    public void testGetAppLinkType_HasText_NoIntent() {
        assertAppLinkType(
                Channel.APP_LINK_TYPE_NONE,
                INVALID_TV_INPUT_PACKAGE_NAME,
                TEST_APP_LINK_TEXT,
                null);
        assertAppLinkType(
                Channel.APP_LINK_TYPE_NONE, LIVE_CHANNELS_PACKAGE_NAME, TEST_APP_LINK_TEXT, null);
        assertAppLinkType(
                Channel.APP_LINK_TYPE_NONE,
                NONE_LEANBACK_TV_INPUT_PACKAGE_NAME,
                TEST_APP_LINK_TEXT,
                null);
        assertAppLinkType(
                Channel.APP_LINK_TYPE_APP,
                LEANBACK_TV_INPUT_PACKAGE_NAME,
                TEST_APP_LINK_TEXT,
                null);
    }

    @Test
    public void testGetAppLinkType_HasText_InvalidIntent() {
        assertAppLinkType(
                Channel.APP_LINK_TYPE_NONE,
                INVALID_TV_INPUT_PACKAGE_NAME,
                TEST_APP_LINK_TEXT,
                mInvalidIntent);
        assertAppLinkType(
                Channel.APP_LINK_TYPE_NONE,
                LIVE_CHANNELS_PACKAGE_NAME,
                TEST_APP_LINK_TEXT,
                mInvalidIntent);
        assertAppLinkType(
                Channel.APP_LINK_TYPE_NONE,
                NONE_LEANBACK_TV_INPUT_PACKAGE_NAME,
                TEST_APP_LINK_TEXT,
                mInvalidIntent);
        assertAppLinkType(
                Channel.APP_LINK_TYPE_APP,
                LEANBACK_TV_INPUT_PACKAGE_NAME,
                TEST_APP_LINK_TEXT,
                mInvalidIntent);
    }

    @Test
    public void testGetAppLinkType_HasText_ValidIntent() {
        assertAppLinkType(
                Channel.APP_LINK_TYPE_CHANNEL,
                INVALID_TV_INPUT_PACKAGE_NAME,
                TEST_APP_LINK_TEXT,
                mValidIntent);
        assertAppLinkType(
                Channel.APP_LINK_TYPE_CHANNEL,
                LIVE_CHANNELS_PACKAGE_NAME,
                TEST_APP_LINK_TEXT,
                mValidIntent);
        assertAppLinkType(
                Channel.APP_LINK_TYPE_CHANNEL,
                NONE_LEANBACK_TV_INPUT_PACKAGE_NAME,
                TEST_APP_LINK_TEXT,
                mValidIntent);
        assertAppLinkType(
                Channel.APP_LINK_TYPE_CHANNEL,
                LEANBACK_TV_INPUT_PACKAGE_NAME,
                TEST_APP_LINK_TEXT,
                mValidIntent);
    }

    private void assertAppLinkType(
            int expectedType, String inputPackageName, String appLinkText, Intent appLinkIntent) {
        // In ChannelImpl, Intent.URI_INTENT_SCHEME is used to parse the URI. So the same flag
        // should be
        // used when the URI is created.
        ChannelImpl testChannel =
                new ChannelImpl.Builder()
                        .setPackageName(inputPackageName)
                        .setAppLinkText(appLinkText)
                        .setAppLinkIntentUri(
                                appLinkIntent == null
                                        ? null
                                        : appLinkIntent.toUri(Intent.URI_INTENT_SCHEME))
                        .build();
        assertWithMessage("Unexpected app-link type for for " + testChannel)
                .that(testChannel.getAppLinkType(mMockContext))
                .isEqualTo(expectedType);
    }

    @Test
    public void testComparator() {
        TvInputManagerHelper manager = Mockito.mock(TvInputManagerHelper.class);
        Mockito.when(manager.isPartnerInput(ArgumentMatchers.anyString()))
                .thenAnswer(
                        new Answer<Boolean>() {
                            @Override
                            public Boolean answer(InvocationOnMock invocation) throws Throwable {
                                String inputId = (String) invocation.getArguments()[0];
                                return PARTNER_INPUT_ID.equals(inputId);
                            }
                        });
        Comparator<Channel> comparator = new TestChannelComparator(manager);
        ComparatorTester comparatorTester =
                new ComparatorTester(comparator).permitInconsistencyWithEquals();
        comparatorTester.addEqualityGroup(
                new ChannelImpl.Builder().setInputId(PARTNER_INPUT_ID).build());
        comparatorTester.addEqualityGroup(new ChannelImpl.Builder().setInputId("1").build());
        comparatorTester.addEqualityGroup(
                new ChannelImpl.Builder().setInputId("1").setDisplayNumber("2").build());
        comparatorTester.addEqualityGroup(
                new ChannelImpl.Builder().setInputId("2").setDisplayNumber("1.0").build());

        // display name does not affect comparator
        comparatorTester.addEqualityGroup(
                new ChannelImpl.Builder()
                        .setInputId("2")
                        .setDisplayNumber("1.62")
                        .setDisplayName("test1")
                        .build(),
                new ChannelImpl.Builder()
                        .setInputId("2")
                        .setDisplayNumber("1.62")
                        .setDisplayName("test2")
                        .build(),
                new ChannelImpl.Builder()
                        .setInputId("2")
                        .setDisplayNumber("1.62")
                        .setDisplayName("test3")
                        .build());
        comparatorTester.addEqualityGroup(
                new ChannelImpl.Builder().setInputId("2").setDisplayNumber("2.0").build());
        // Numeric display number sorting
        comparatorTester.addEqualityGroup(
                new ChannelImpl.Builder().setInputId("2").setDisplayNumber("12.2").build());
        comparatorTester.testCompare();
    }

    /**
     * Test Input Label handled by {@link ChannelImpl.DefaultComparator}.
     *
     * <p>Sort partner inputs first, then sort by input label, then by input id. See <a
     * href="http://b/23031603">b/23031603</a>.
     */
    @Test
    public void testComparatorLabel() {
        TvInputManagerHelper manager = Mockito.mock(TvInputManagerHelper.class);
        Mockito.when(manager.isPartnerInput(ArgumentMatchers.anyString()))
                .thenAnswer(
                        new Answer<Boolean>() {
                            @Override
                            public Boolean answer(InvocationOnMock invocation) throws Throwable {
                                String inputId = (String) invocation.getArguments()[0];
                                return PARTNER_INPUT_ID.equals(inputId);
                            }
                        });
        Comparator<Channel> comparator = new ChannelComparatorWithDescriptionAsLabel(manager);
        ComparatorTester comparatorTester =
                new ComparatorTester(comparator).permitInconsistencyWithEquals();

        comparatorTester.addEqualityGroup(
                new ChannelImpl.Builder().setInputId(PARTNER_INPUT_ID).setDescription("A").build());

        // The description is used as a label for this test.
        comparatorTester.addEqualityGroup(
                new ChannelImpl.Builder().setDescription("A").setInputId("1").build());
        comparatorTester.addEqualityGroup(
                new ChannelImpl.Builder().setDescription("A").setInputId("2").build());
        comparatorTester.addEqualityGroup(
                new ChannelImpl.Builder().setDescription("B").setInputId("1").build());

        comparatorTester.testCompare();
    }

    @Test
    public void testNormalizeChannelNumber() {
        assertNormalizedDisplayNumber(null, null);
        assertNormalizedDisplayNumber("", "");
        assertNormalizedDisplayNumber("1", "1");
        assertNormalizedDisplayNumber("abcde", "abcde");
        assertNormalizedDisplayNumber("1-1", "1-1");
        assertNormalizedDisplayNumber("1.1", "1-1");
        assertNormalizedDisplayNumber("1 1", "1-1");
        assertNormalizedDisplayNumber("1\u058a1", "1-1");
        assertNormalizedDisplayNumber("1\u05be1", "1-1");
        assertNormalizedDisplayNumber("1\u14001", "1-1");
        assertNormalizedDisplayNumber("1\u18061", "1-1");
        assertNormalizedDisplayNumber("1\u20101", "1-1");
        assertNormalizedDisplayNumber("1\u20111", "1-1");
        assertNormalizedDisplayNumber("1\u20121", "1-1");
        assertNormalizedDisplayNumber("1\u20131", "1-1");
        assertNormalizedDisplayNumber("1\u20141", "1-1");
    }

    private void assertNormalizedDisplayNumber(String displayNumber, String normalized) {
        assertThat(ChannelImpl.normalizeDisplayNumber(displayNumber)).isEqualTo(normalized);
    }

    private static final class TestChannelComparator extends ChannelImpl.DefaultComparator {
        public TestChannelComparator(TvInputManagerHelper manager) {
            super(null, manager);
        }

        @Override
        public String getInputLabelForChannel(Channel channel) {
            return channel.getInputId();
        }
    }

    private static final class ChannelComparatorWithDescriptionAsLabel
            extends ChannelImpl.DefaultComparator {
        public ChannelComparatorWithDescriptionAsLabel(TvInputManagerHelper manager) {
            super(null, manager);
        }

        @Override
        public String getInputLabelForChannel(Channel channel) {
            return channel.getDescription();
        }
    }
}
