/*
 * Copyright (C) 2008 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 android.view;

import android.graphics.Rect;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.SmallTest;

public class FocusFinderTest extends AndroidTestCase {

    private FocusFinderHelper mFocusFinder;

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        mFocusFinder = new FocusFinderHelper(FocusFinder.getInstance());
    }

    @SmallTest
    public void testPreconditions() {
        assertNotNull("focus finder instance", mFocusFinder);
    }

    @SmallTest
    public void testBelowNotCandidateForDirectionUp() {
        assertIsNotCandidate(View.FOCUS_UP,
                new Rect(0, 30, 10, 40),  // src  (left, top, right, bottom)
                new Rect(0, 50, 10, 60));  // dest (left, top, right, bottom)
    }

    @SmallTest
    public void testAboveShareEdgeEdgeOkForDirectionUp() {
        final Rect src = new Rect(0, 30, 10, 40);

        final Rect dest = new Rect(src);
        dest.offset(0, -src.height());
        assertEquals(src.top, dest.bottom);

        assertDirectionIsCandidate(View.FOCUS_UP, src, dest);
    }

    @SmallTest
    public void testCompletelyContainedNotCandidate() {
        assertIsNotCandidate(
                View.FOCUS_DOWN,
                //       L  T   R   B
                new Rect(0, 0,  50, 50),
                new Rect(0, 1,  50, 49));
    }

    @SmallTest
    public void testContinaedWithCommonBottomNotCandidate() {
        assertIsNotCandidate(
                View.FOCUS_DOWN,
                //       L  T   R   B
                new Rect(0, 0,  50, 50),
                new Rect(0, 1,  50, 50));
    }

    @SmallTest
    public void testOverlappingIsCandidateWhenBothEdgesAreInDirection() {
        assertDirectionIsCandidate(
                View.FOCUS_DOWN,
                //       L  T   R   B
                new Rect(0, 0,  50, 50),
                new Rect(0, 1,  50, 51));
    }

    @SmallTest
    public void testTopEdgeOfDestAtOrAboveTopOfSrcNotCandidateForDown() {
        assertIsNotCandidate(
                View.FOCUS_DOWN,
                //       L  T   R   B
                new Rect(0, 0,  50, 50),
                new Rect(0, 0,  50, 51));
        assertIsNotCandidate(
                View.FOCUS_DOWN,
                //       L  T   R   B
                new Rect(0, 0,  50, 50),
                new Rect(0, -1, 50, 51));
    }

    @SmallTest
    public void testSameRectBeamsOverlap() {
        final Rect rect = new Rect(0, 0, 20, 20);

        assertBeamsOverlap(View.FOCUS_LEFT, rect, rect);
        assertBeamsOverlap(View.FOCUS_RIGHT, rect, rect);
        assertBeamsOverlap(View.FOCUS_UP, rect, rect);
        assertBeamsOverlap(View.FOCUS_DOWN, rect, rect);
    }

    @SmallTest
    public void testOverlapBeamsRightLeftUpToEdge() {
        final Rect rect1 = new Rect(0, 0, 20, 20);
        final Rect rect2 = new Rect(rect1);

        // just below bottom edge
        rect2.offset(0, rect1.height() - 1);
        assertBeamsOverlap(View.FOCUS_LEFT, rect1, rect2);
        assertBeamsOverlap(View.FOCUS_RIGHT, rect1, rect2);

        // at edge
        rect2.offset(0, 1);
        assertBeamsOverlap(View.FOCUS_LEFT, rect1, rect2);
        assertBeamsOverlap(View.FOCUS_RIGHT, rect1, rect2);

        // just beyond
        rect2.offset(0, 1);
        assertBeamsDontOverlap(View.FOCUS_LEFT, rect1, rect2);
        assertBeamsDontOverlap(View.FOCUS_RIGHT, rect1, rect2);

        // just below top edge
        rect2.set(rect1);
        rect2.offset(0, -(rect1.height() - 1));
        assertBeamsOverlap(View.FOCUS_LEFT, rect1, rect2);
        assertBeamsOverlap(View.FOCUS_RIGHT, rect1, rect2);

        // at top edge
        rect2.offset(0, -1);
        assertBeamsOverlap(View.FOCUS_LEFT, rect1, rect2);
        assertBeamsOverlap(View.FOCUS_RIGHT, rect1, rect2);

        // just beyond top edge
        rect2.offset(0, -1);
        assertBeamsDontOverlap(View.FOCUS_LEFT, rect1, rect2);
        assertBeamsDontOverlap(View.FOCUS_RIGHT, rect1, rect2);
    }

    @SmallTest
    public void testOverlapBeamsUpDownUpToEdge() {
        final Rect rect1 = new Rect(0, 0, 20, 20);
        final Rect rect2 = new Rect(rect1);

        // just short of right edge
        rect2.offset(rect1.width() - 1, 0);
        assertBeamsOverlap(View.FOCUS_UP, rect1, rect2);
        assertBeamsOverlap(View.FOCUS_DOWN, rect1, rect2);

        // at edge
        rect2.offset(1, 0);
        assertBeamsOverlap(View.FOCUS_UP, rect1, rect2);
        assertBeamsOverlap(View.FOCUS_DOWN, rect1, rect2);

        // just beyond
        rect2.offset(1, 0);
        assertBeamsDontOverlap(View.FOCUS_UP, rect1, rect2);
        assertBeamsDontOverlap(View.FOCUS_DOWN, rect1, rect2);

        // just short of left edge
        rect2.set(rect1);
        rect2.offset(-(rect1.width() - 1), 0);
        assertBeamsOverlap(View.FOCUS_UP, rect1, rect2);
        assertBeamsOverlap(View.FOCUS_DOWN, rect1, rect2);

        // at edge
        rect2.offset(-1, 0);
        assertBeamsOverlap(View.FOCUS_UP, rect1, rect2);
        assertBeamsOverlap(View.FOCUS_DOWN, rect1, rect2);

        // just beyond edge
        rect2.offset(-1, 0);
        assertBeamsDontOverlap(View.FOCUS_UP, rect1, rect2);
        assertBeamsDontOverlap(View.FOCUS_DOWN, rect1, rect2);
    }

    @SmallTest
    public void testDirectlyAboveTrumpsAboveLeft() {
        Rect src = new Rect(0, 50, 20, 70);  // src (left, top, right, bottom)

        Rect directlyAbove = new Rect(src);
        directlyAbove.offset(0, -(1 + src.height()));

        Rect aboveLeft = new Rect(src);
        aboveLeft.offset(-(1 + src.width()), -(1 + src.height()));

        assertBetterCandidate(View.FOCUS_UP, src, directlyAbove, aboveLeft);
    }

    @SmallTest
    public void testAboveInBeamTrumpsSlightlyCloserOutOfBeam() {
        Rect src = new Rect(0, 50, 20, 70);  // src (left, top, right, bottom)

        Rect directlyAbove = new Rect(src);
        directlyAbove.offset(0, -(1 + src.height()));

        Rect aboveLeft = new Rect(src);
        aboveLeft.offset(-(1 + src.width()), -(1 + src.height()));

        // offset directly above a little further up
        directlyAbove.offset(0, -5);
        assertBetterCandidate(View.FOCUS_UP, src, directlyAbove, aboveLeft);
    }

    @SmallTest
    public void testOutOfBeamBeatsInBeamUp() {

        Rect src = new Rect(0, 0, 50, 50); // (left, top, right, bottom)

        Rect aboveLeftOfBeam = new Rect(src);
        aboveLeftOfBeam.offset(-(src.width() + 1), -src.height());
        assertBeamsDontOverlap(View.FOCUS_UP, src, aboveLeftOfBeam);

        Rect aboveInBeam = new Rect(src);
        aboveInBeam.offset(0, -src.height());
        assertBeamsOverlap(View.FOCUS_UP, src, aboveInBeam);

        // in beam wins
        assertBetterCandidate(View.FOCUS_UP, src, aboveInBeam, aboveLeftOfBeam);

        // still wins while aboveInBeam's bottom edge is < out of beams' top
        aboveInBeam.offset(0, -(aboveLeftOfBeam.height() - 1));
        assertTrue("aboveInBeam.bottom > aboveLeftOfBeam.top", aboveInBeam.bottom > aboveLeftOfBeam.top);
        assertBetterCandidate(View.FOCUS_UP, src, aboveInBeam, aboveLeftOfBeam);

        // cross the threshold: the out of beam prevails
        aboveInBeam.offset(0, -1);
        assertEquals(aboveInBeam.bottom, aboveLeftOfBeam.top);
        assertBetterCandidate(View.FOCUS_UP, src, aboveLeftOfBeam, aboveInBeam);
    }

    /**
     * A non-candidate (even a much closer one) is always a worse choice
     * than a real candidate.
     */
    @MediumTest
    public void testSomeCandidateBetterThanNonCandidate() {
        Rect src = new Rect(0, 0, 50, 50); // (left, top, right, bottom)

        Rect nonCandidate = new Rect(src);
        nonCandidate.offset(src.width() + 1, 0);

        assertIsNotCandidate(View.FOCUS_LEFT, src, nonCandidate);

        Rect candidate = new Rect(src);
        candidate.offset(-(4 * src.width()), 0);
        assertDirectionIsCandidate(View.FOCUS_LEFT, src, candidate);

        assertBetterCandidate(View.FOCUS_LEFT, src, candidate, nonCandidate);
    }

    /**
     * Grabbed from {@link android.widget.focus.VerticalFocusSearchTest#testSearchFromMidLeft()}
     */
    @SmallTest
    public void testVerticalFocusSearchScenario() {
        assertBetterCandidate(View.FOCUS_DOWN,
                //       L    T    R    B
                new Rect(0,   109, 153, 169),   // src
                new Rect(166, 169, 319, 229),  // expectedbetter
                new Rect(0,   229, 320, 289)); // expectedworse

        // failing test 4/10/2008, the values were tweaked somehow in functional
        // test...
        assertBetterCandidate(View.FOCUS_DOWN,
                //       L    T    R    B
                new Rect(0,   91, 153, 133),   // src
                new Rect(166, 133, 319, 175),  // expectedbetter
                new Rect(0,   175, 320, 217)); // expectedworse

    }

    /**
     * Example: going down from a thin button all the way to the left of a
     * screen where, just below, is a very wide button, and just below that,
     * is an equally skinny button all the way to the left.  want to make
     * sure any minor axis factor doesn't override the fact that the one below
     * in vertical beam should be next focus
     */
    @SmallTest
    public void testBeamsOverlapMajorAxisCloserMinorAxisFurther() {
        assertBetterCandidate(View.FOCUS_DOWN,
                //       L   T    R    B
                new Rect(0,  0,   100,  100),  // src
                new Rect(0,  100, 480,  200),  // expectedbetter
                new Rect(0,  200, 100,  300)); // expectedworse
    }

    /**
     * Real scenario grabbed from song playback screen.
     */
    @SmallTest
    public void testMusicPlaybackScenario() {
        assertBetterCandidate(View.FOCUS_LEFT,
                //       L    T    R    B
                new Rect(227, 185, 312, 231),   // src
                new Rect(195, 386, 266, 438),   // expectedbetter
                new Rect(124, 386, 195, 438));  // expectedworse
    }

    /**
     * more generalized version of {@link #testMusicPlaybackScenario()}
     */
    @SmallTest
    public void testOutOfBeamOverlapBeatsOutOfBeamFurtherOnMajorAxis() {
        assertBetterCandidate(View.FOCUS_DOWN,
                //       L    T    R    B
                new Rect(0,   0,   50,  50),   // src
                new Rect(60,  40,  110, 90),   // expectedbetter
                new Rect(60,  70,  110, 120));  // expectedworse
    }

    /**
     * Make sure that going down prefers views that are actually
     * down (and not those next to but still a candidate because
     * they are overlapping on the major axis)
     */
    @SmallTest
    public void testInBeamTrumpsOutOfBeamOverlapping() {
        assertBetterCandidate(View.FOCUS_DOWN,
                //       L    T    R    B
                new Rect(0,   0,   50,  50),   // src
                new Rect(0,   60,  50,  110),  // expectedbetter
                new Rect(51,  1,   101, 51)); // expectedworse
    }

    @SmallTest
    public void testOverlappingBeatsNonOverlapping() {
        assertBetterCandidate(View.FOCUS_DOWN,
                //       L    T    R    B
                new Rect(0,   0,   50,  50),   // src
                new Rect(0,   40,  50,  90),   // expectedbetter
                new Rect(0,   75,  50,  125)); // expectedworse
    }

    @SmallTest
    public void testEditContactScenarioLeftFromDiscardChangesGoesToSaveContactInLandscape() {
        assertBetterCandidate(View.FOCUS_LEFT,
                //       L    T    R    B
                new Rect(357, 258, 478, 318),  // src
                new Rect(2,   258, 100, 318),  // better
                new Rect(106, 120, 424, 184)); // worse
    }

    /**
     * A dial pad with 9 squares arranged in a grid.  no padding, so
     * the edges are equal.  see {@link android.widget.focus.LinearLayoutGrid}
     */
    @SmallTest
    public void testGridWithTouchingEdges() {
        assertBetterCandidate(View.FOCUS_DOWN,
                //       L    T    R    B
                new Rect(106, 49,  212, 192),  // src
                new Rect(106, 192, 212, 335),  // better
                new Rect(0,   192, 106, 335)); // worse

        assertBetterCandidate(View.FOCUS_DOWN,
                //       L    T    R    B
                new Rect(106, 49,  212, 192),  // src
                new Rect(106, 192, 212, 335),  // better
                new Rect(212, 192, 318, 335)); // worse
    }

    @SmallTest
    public void testSearchFromEmptyRect() {
        assertBetterCandidate(View.FOCUS_DOWN,
                //       L   T    R    B
                new Rect(0,  0,   0,   0),    // src
                new Rect(0,  0,   320, 45),   // better
                new Rect(0,  45,  320, 545)); // worse
    }

    /**
     * Reproduce bug 1124559, drilling down to actual bug
     * (majorAxisDistance was wrong for direction left)
     */
    @SmallTest
    public void testGmailReplyButtonsScenario() {
        assertBetterCandidate(View.FOCUS_LEFT,
                //       L    T    R    B
                new Rect(223, 380, 312, 417),  // src
                new Rect(102, 380, 210, 417),  // better
                new Rect(111, 443, 206, 480)); // worse

        assertBeamBeats(View.FOCUS_LEFT,
            //       L    T    R    B
            new Rect(223, 380, 312, 417),  // src
            new Rect(102, 380, 210, 417),  // better
            new Rect(111, 443, 206, 480)); // worse

        assertBeamsOverlap(View.FOCUS_LEFT,
                //       L    T    R    B
                new Rect(223, 380, 312, 417),
                new Rect(102, 380, 210, 417));

        assertBeamsDontOverlap(View.FOCUS_LEFT,
                //       L    T    R    B
                new Rect(223, 380, 312, 417),
                new Rect(111, 443, 206, 480));

        assertTrue(
                "major axis distance less than major axis distance to "
                        + "far edge",
                FocusFinderHelper.majorAxisDistance(View.FOCUS_LEFT,
                        //       L    T    R    B
                        new Rect(223, 380, 312, 417),
                        new Rect(102, 380, 210, 417)) <
                FocusFinderHelper.majorAxisDistanceToFarEdge(View.FOCUS_LEFT,
                        //       L    T    R    B
                        new Rect(223, 380, 312, 417),
                        new Rect(111, 443, 206, 480)));
    }

    @SmallTest
    public void testGmailScenarioBug1203288() {
        assertBetterCandidate(View.FOCUS_DOWN,
                //       L    T    R    B
                new Rect(0,   2,   480, 82),   // src
                new Rect(344, 87,  475, 124),  // better
                new Rect(0,   130, 480, 203)); // worse
    }

    @SmallTest
    public void testHomeShortcutScenarioBug1295354() {
        assertBetterCandidate(View.FOCUS_RIGHT,
                //       L    T    R    B
                new Rect(3, 338, 77, 413),   // src
                new Rect(163, 338, 237, 413),  // better
                new Rect(83, 38, 157, 113)); // worse
    }

    @SmallTest
    public void testBeamAlwaysBeatsHoriz() {
        assertBetterCandidate(View.FOCUS_RIGHT,
                //       L    T    R    B
                new Rect(0,   0,   50,  50),   // src
                new Rect(150, 0,   200, 50),   // better, (way further, but in beam)
                new Rect(60,  51,  110, 101)); // worse, even though it is closer 

        assertBetterCandidate(View.FOCUS_LEFT,
                //       L    T    R    B
                new Rect(150, 0,   200,  50),   // src
                new Rect(0,   50,  50,   50),   // better, (way further, but in beam)
                new Rect(49,  99,  149,  101)); // worse, even though it is closer 
    }

    @SmallTest
    public void testIsCandidateOverlappingEdgeFromEmptyRect() {
        assertDirectionIsCandidate(View.FOCUS_DOWN,
                //       L   T    R    B
                new Rect(0,  0,   0,   0),   // src
                new Rect(0,  0,   20,  1));  // candidate

        assertDirectionIsCandidate(View.FOCUS_UP,
                //       L   T    R    B
                new Rect(0,  0,   0,   0),   // src
                new Rect(0,  -1,  20,  0));  // candidate

        assertDirectionIsCandidate(View.FOCUS_LEFT,
                //       L   T    R    B
                new Rect(0,  0,   0,   0),    // src
                new Rect(-1,  0,  0,   20));  // candidate

        assertDirectionIsCandidate(View.FOCUS_RIGHT,
                //       L   T    R    B
                new Rect(0,  0,   0,   0),    // src
                new Rect(0,  0,   1,   20));  // candidate
    }

    private void assertBeamsOverlap(int direction, Rect rect1, Rect rect2) {
        String directionStr = validateAndGetStringFor(direction);
        String assertMsg = String.format("Expected beams to overlap in direction %s "
                + "for rectangles %s and %s", directionStr, rect1, rect2);
        assertTrue(assertMsg, mFocusFinder.beamsOverlap(direction, rect1, rect2));
    }

    private void assertBeamsDontOverlap(int direction, Rect rect1, Rect rect2) {
        String directionStr = validateAndGetStringFor(direction);
        String assertMsg = String.format("Expected beams not to overlap in direction %s "
                + "for rectangles %s and %s", directionStr, rect1, rect2);
        assertFalse(assertMsg, mFocusFinder.beamsOverlap(direction, rect1, rect2));
    }

    /**
     * Assert that particular rect is a better focus search candidate from a
     * source rect than another.
     * @param direction The direction of focus search.
     * @param srcRect The src rectangle.
     * @param expectedBetter The candidate that should be better.
     * @param expectedWorse The candidate that should be worse.
     */
    private void assertBetterCandidate(int direction, Rect srcRect,
            Rect expectedBetter, Rect expectedWorse) {

        String directionStr = validateAndGetStringFor(direction);
        String assertMsg = String.format(
                "expected %s to be a better focus search candidate than "
                        + "%s when searching "
                        + "from %s in direction %s",
                expectedBetter, expectedWorse, srcRect, directionStr);

        assertTrue(assertMsg,
                mFocusFinder.isBetterCandidate(direction, srcRect,
                        expectedBetter, expectedWorse));

        assertMsg = String.format(
                "expected %s to not be a better focus search candidate than "
                        + "%s when searching "
                        + "from %s in direction %s",
                expectedWorse, expectedBetter, srcRect, directionStr);

        assertFalse(assertMsg,
                mFocusFinder.isBetterCandidate(direction, srcRect,
                        expectedWorse, expectedBetter));
    }

    private void assertIsNotCandidate(int direction, Rect src, Rect dest) {
        String directionStr = validateAndGetStringFor(direction);

        final String assertMsg = String.format(
                "expected going from %s to %s in direction %s to be an invalid "
                        + "focus search candidate",
                src, dest, directionStr);
        assertFalse(assertMsg, mFocusFinder.isCandidate(src, dest, direction));
    }

    private void assertBeamBeats(int direction, Rect srcRect,
            Rect rect1, Rect rect2) {

        String directionStr = validateAndGetStringFor(direction);
        String assertMsg = String.format(
                "expecting %s to beam beat %s w.r.t %s in direction %s",
                rect1, rect2, srcRect, directionStr);
        assertTrue(assertMsg, mFocusFinder.beamBeats(direction, srcRect, rect1, rect2));
    }


    private void assertDirectionIsCandidate(int direction, Rect src, Rect dest) {
        String directionStr = validateAndGetStringFor(direction);

        final String assertMsg = String.format(
                "expected going from %s to %s in direction %s to be a valid "
                        + "focus search candidate",
                src, dest, directionStr);
        assertTrue(assertMsg, mFocusFinder.isCandidate(src, dest, direction));
    }

    private String validateAndGetStringFor(int direction) {
        String directionStr = "??";
        switch(direction) {
            case View.FOCUS_UP:
                directionStr = "FOCUS_UP";
                break;
            case View.FOCUS_DOWN:
                directionStr = "FOCUS_DOWN";
                break;
            case View.FOCUS_LEFT:
                directionStr = "FOCUS_LEFT";
                break;
            case View.FOCUS_RIGHT:
                directionStr = "FOCUS_RIGHT";
                break;
            default:
                fail("passed in unknown direction, ya blewit!");
        }
        return directionStr;
    }


}
