// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "chrome/browser/autocomplete/autocomplete_popup_view_mac.h"

#include "app/text_elider.h"
#include "base/scoped_ptr.h"
#include "base/sys_string_conversions.h"
#include "chrome/browser/autocomplete/autocomplete.h"
#include "testing/platform_test.h"

namespace {

const float kLargeWidth = 10000;

class AutocompletePopupViewMacTest : public PlatformTest {
 public:
  AutocompletePopupViewMacTest() {}

  virtual void SetUp() {
    PlatformTest::SetUp();

    // These are here because there is no autorelease pool for the
    // constructor.
    color_ = [NSColor blackColor];
    font_ = gfx::Font(
        base::SysNSStringToWide([[NSFont userFontOfSize:12] fontName]), 12);
  }

  // Returns the length of the run starting at |location| for which
  // |attributeName| remains the same.
  static NSUInteger RunLengthForAttribute(NSAttributedString* string,
                                          NSUInteger location,
                                          NSString* attributeName) {
    const NSRange fullRange = NSMakeRange(0, [string length]);
    NSRange range;
    [string attribute:attributeName
              atIndex:location longestEffectiveRange:&range inRange:fullRange];

    // In order to signal when the run doesn't start exactly at
    // location, return a weirdo length.  This causes the incorrect
    // expectation to manifest at the calling location, which is more
    // useful than an EXPECT_EQ() would be here.
    if (range.location != location) {
      return -1;
    }

    return range.length;
  }

  // Return true if the run starting at |location| has |color| for
  // attribute NSForegroundColorAttributeName.
  static bool RunHasColor(NSAttributedString* string,
                          NSUInteger location, NSColor* color) {
    const NSRange fullRange = NSMakeRange(0, [string length]);
    NSRange range;
    NSColor* runColor = [string attribute:NSForegroundColorAttributeName
                                  atIndex:location
                    longestEffectiveRange:&range inRange:fullRange];

    // According to one "Ali Ozer", you can compare objects within the
    // same color space using -isEqual:.  Converting color spaces
    // seems too heavyweight for these tests.
    // http://lists.apple.com/archives/cocoa-dev/2005/May/msg00186.html
    return [runColor isEqual:color] ? true : false;
  }

  // Return true if the run starting at |location| has the font
  // trait(s) in |mask| font in NSFontAttributeName.
  static bool RunHasFontTrait(NSAttributedString* string, NSUInteger location,
                              NSFontTraitMask mask) {
    const NSRange fullRange = NSMakeRange(0, [string length]);
    NSRange range;
    NSFont* runFont = [string attribute:NSFontAttributeName
                                atIndex:location
                  longestEffectiveRange:&range inRange:fullRange];
    NSFontManager* fontManager = [NSFontManager sharedFontManager];
    if (runFont && (mask == ([fontManager traitsOfFont:runFont]&mask))) {
      return true;
    }
    return false;
  }

  // AutocompleteMatch doesn't really have the right constructor for our
  // needs.  Fake one for us to use.
  static AutocompleteMatch MakeMatch(const std::wstring &contents,
                                     const std::wstring &description) {
    AutocompleteMatch m(NULL, 1, true, AutocompleteMatch::URL_WHAT_YOU_TYPED);
    m.contents = contents;
    m.description = description;
    return m;
  }

  NSColor* color_;  // weak
  gfx::Font font_;
};

// Simple inputs with no matches should result in styled output who's
// text matches the input string, with the passed-in color, and
// nothing bolded.
TEST_F(AutocompletePopupViewMacTest, DecorateMatchedStringNoMatch) {
  NSString* const string = @"This is a test";
  AutocompleteMatch::ACMatchClassifications classifications;

  NSAttributedString* decorated =
      AutocompletePopupViewMac::DecorateMatchedString(
          base::SysNSStringToWide(string), classifications,
          color_, font_);

  // Result has same characters as the input.
  EXPECT_EQ([decorated length], [string length]);
  EXPECT_TRUE([[decorated string] isEqualToString:string]);

  // Our passed-in color for the entire string.
  EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
                                  NSForegroundColorAttributeName),
            [string length]);
  EXPECT_TRUE(RunHasColor(decorated, 0U, color_));

  // An unbolded font for the entire string.
  EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
                                  NSFontAttributeName), [string length]);
  EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));
}

// Identical to DecorateMatchedStringNoMatch, except test that URL
// style gets a different color than we passed in.
TEST_F(AutocompletePopupViewMacTest, DecorateMatchedStringURLNoMatch) {
  NSString* const string = @"This is a test";
  AutocompleteMatch::ACMatchClassifications classifications;

  classifications.push_back(
      ACMatchClassification(0, ACMatchClassification::URL));

  NSAttributedString* decorated =
      AutocompletePopupViewMac::DecorateMatchedString(
          base::SysNSStringToWide(string), classifications,
          color_, font_);

  // Result has same characters as the input.
  EXPECT_EQ([decorated length], [string length]);
  EXPECT_TRUE([[decorated string] isEqualToString:string]);

  // One color for the entire string, and it's not the one we passed in.
  EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
                                  NSForegroundColorAttributeName),
            [string length]);
  EXPECT_FALSE(RunHasColor(decorated, 0U, color_));

  // An unbolded font for the entire string.
  EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
                                  NSFontAttributeName), [string length]);
  EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));
}

// Test that DIM doesn't have any impact - true at this time.
TEST_F(AutocompletePopupViewMacTest, DecorateMatchedStringDimNoMatch) {
  NSString* const string = @"This is a test";

  // Switch to DIM halfway through.
  AutocompleteMatch::ACMatchClassifications classifications;
  classifications.push_back(
      ACMatchClassification(0, ACMatchClassification::NONE));
  classifications.push_back(
      ACMatchClassification([string length] / 2, ACMatchClassification::DIM));

  NSAttributedString* decorated =
      AutocompletePopupViewMac::DecorateMatchedString(
          base::SysNSStringToWide(string), classifications,
          color_, font_);

  // Result has same characters as the input.
  EXPECT_EQ([decorated length], [string length]);
  EXPECT_TRUE([[decorated string] isEqualToString:string]);

  // Our passed-in color for the entire string.
  EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
                                  NSForegroundColorAttributeName),
            [string length]);
  EXPECT_TRUE(RunHasColor(decorated, 0U, color_));

  // An unbolded font for the entire string.
  EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
                                  NSFontAttributeName), [string length]);
  EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));
}

// Test that the matched run gets bold-faced, but keeps the same
// color.
TEST_F(AutocompletePopupViewMacTest, DecorateMatchedStringMatch) {
  NSString* const string = @"This is a test";
  // Match "is".
  const NSUInteger runLength1 = 5, runLength2 = 2, runLength3 = 7;
  // Make sure nobody messed up the inputs.
  EXPECT_EQ(runLength1 + runLength2 + runLength3, [string length]);

  // Push each run onto classifications.
  AutocompleteMatch::ACMatchClassifications classifications;
  classifications.push_back(
      ACMatchClassification(0, ACMatchClassification::NONE));
  classifications.push_back(
      ACMatchClassification(runLength1, ACMatchClassification::MATCH));
  classifications.push_back(
      ACMatchClassification(runLength1 + runLength2,
                            ACMatchClassification::NONE));

  NSAttributedString* decorated =
      AutocompletePopupViewMac::DecorateMatchedString(
          base::SysNSStringToWide(string), classifications,
          color_, font_);

  // Result has same characters as the input.
  EXPECT_EQ([decorated length], [string length]);
  EXPECT_TRUE([[decorated string] isEqualToString:string]);

  // Our passed-in color for the entire string.
  EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
                                  NSForegroundColorAttributeName),
            [string length]);
  EXPECT_TRUE(RunHasColor(decorated, 0U, color_));

  // Should have three font runs, not bold, bold, then not bold again.
  EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
                                  NSFontAttributeName), runLength1);
  EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));

  EXPECT_EQ(RunLengthForAttribute(decorated, runLength1,
                                  NSFontAttributeName), runLength2);
  EXPECT_TRUE(RunHasFontTrait(decorated, runLength1, NSBoldFontMask));

  EXPECT_EQ(RunLengthForAttribute(decorated, runLength1 + runLength2,
                                  NSFontAttributeName), runLength3);
  EXPECT_FALSE(RunHasFontTrait(decorated, runLength1 + runLength2,
                               NSBoldFontMask));
}

// Just like DecorateMatchedStringURLMatch, this time with URL style.
TEST_F(AutocompletePopupViewMacTest, DecorateMatchedStringURLMatch) {
  NSString* const string = @"http://hello.world/";
  // Match "hello".
  const NSUInteger runLength1 = 7, runLength2 = 5, runLength3 = 7;
  // Make sure nobody messed up the inputs.
  EXPECT_EQ(runLength1 + runLength2 + runLength3, [string length]);

  // Push each run onto classifications.
  AutocompleteMatch::ACMatchClassifications classifications;
  classifications.push_back(
      ACMatchClassification(0, ACMatchClassification::URL));
  const int kURLMatch = ACMatchClassification::URL|ACMatchClassification::MATCH;
  classifications.push_back(ACMatchClassification(runLength1, kURLMatch));
  classifications.push_back(
      ACMatchClassification(runLength1 + runLength2,
                            ACMatchClassification::URL));

  NSAttributedString* decorated =
      AutocompletePopupViewMac::DecorateMatchedString(
          base::SysNSStringToWide(string), classifications,
          color_, font_);

  // Result has same characters as the input.
  EXPECT_EQ([decorated length], [string length]);
  EXPECT_TRUE([[decorated string] isEqualToString:string]);

  // One color for the entire string, and it's not the one we passed in.
  EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
                                  NSForegroundColorAttributeName),
            [string length]);
  EXPECT_FALSE(RunHasColor(decorated, 0U, color_));

  // Should have three font runs, not bold, bold, then not bold again.
  EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
                                  NSFontAttributeName), runLength1);
  EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));

  EXPECT_EQ(RunLengthForAttribute(decorated, runLength1,
                                  NSFontAttributeName), runLength2);
  EXPECT_TRUE(RunHasFontTrait(decorated, runLength1, NSBoldFontMask));

  EXPECT_EQ(RunLengthForAttribute(decorated, runLength1 + runLength2,
                                  NSFontAttributeName), runLength3);
  EXPECT_FALSE(RunHasFontTrait(decorated, runLength1 + runLength2,
                               NSBoldFontMask));
}

// Check that matches with both contents and description come back
// with contents at the beginning, description at the end, and
// something separating them.  Not being specific about the separator
// on purpose, in case it changes.
TEST_F(AutocompletePopupViewMacTest, MatchText) {
  NSString* const contents = @"contents";
  NSString* const description = @"description";
  AutocompleteMatch m = MakeMatch(base::SysNSStringToWide(contents),
                                  base::SysNSStringToWide(description));

  NSAttributedString* decorated =
      AutocompletePopupViewMac::MatchText(m, font_, kLargeWidth);

  // Result contains the characters of the input in the right places.
  EXPECT_GT([decorated length], [contents length] + [description length]);
  EXPECT_TRUE([[decorated string] hasPrefix:contents]);
  EXPECT_TRUE([[decorated string] hasSuffix:description]);

  // Check that the description is a different color from the
  // contents.
  const NSUInteger descriptionLocation =
      [decorated length] - [description length];
  EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
                                  NSForegroundColorAttributeName),
            descriptionLocation);
  EXPECT_EQ(RunLengthForAttribute(decorated, descriptionLocation,
                                  NSForegroundColorAttributeName),
            [description length]);

  // Same font all the way through, nothing bold.
  EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
                                  NSFontAttributeName), [decorated length]);
  EXPECT_FALSE(RunHasFontTrait(decorated, 0, NSBoldFontMask));
}

// Check that MatchText() styles content matches as expected.
TEST_F(AutocompletePopupViewMacTest, MatchTextContentsMatch) {
  NSString* const contents = @"This is a test";
  // Match "is".
  const NSUInteger runLength1 = 5, runLength2 = 2, runLength3 = 7;
  // Make sure nobody messed up the inputs.
  EXPECT_EQ(runLength1 + runLength2 + runLength3, [contents length]);

  AutocompleteMatch m = MakeMatch(base::SysNSStringToWide(contents),
                                  std::wstring());

  // Push each run onto contents classifications.
  m.contents_class.push_back(
      ACMatchClassification(0, ACMatchClassification::NONE));
  m.contents_class.push_back(
      ACMatchClassification(runLength1, ACMatchClassification::MATCH));
  m.contents_class.push_back(
      ACMatchClassification(runLength1 + runLength2,
                            ACMatchClassification::NONE));

  NSAttributedString* decorated =
      AutocompletePopupViewMac::MatchText(m, font_, kLargeWidth);

  // Result has same characters as the input.
  EXPECT_EQ([decorated length], [contents length]);
  EXPECT_TRUE([[decorated string] isEqualToString:contents]);

  // Result is all one color.
  EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
                                  NSForegroundColorAttributeName),
            [contents length]);

  // Should have three font runs, not bold, bold, then not bold again.
  EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
                                  NSFontAttributeName), runLength1);
  EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));

  EXPECT_EQ(RunLengthForAttribute(decorated, runLength1,
                                  NSFontAttributeName), runLength2);
  EXPECT_TRUE(RunHasFontTrait(decorated, runLength1, NSBoldFontMask));

  EXPECT_EQ(RunLengthForAttribute(decorated, runLength1 + runLength2,
                                  NSFontAttributeName), runLength3);
  EXPECT_FALSE(RunHasFontTrait(decorated, runLength1 + runLength2,
                               NSBoldFontMask));
}

// Check that MatchText() styles description matches as expected.
TEST_F(AutocompletePopupViewMacTest, MatchTextDescriptionMatch) {
  NSString* const contents = @"This is a test";
  NSString* const description = @"That was a test";
  // Match "That was".
  const NSUInteger runLength1 = 8, runLength2 = 7;
  // Make sure nobody messed up the inputs.
  EXPECT_EQ(runLength1 + runLength2, [description length]);

  AutocompleteMatch m = MakeMatch(base::SysNSStringToWide(contents),
                                  base::SysNSStringToWide(description));

  // Push each run onto contents classifications.
  m.description_class.push_back(
      ACMatchClassification(0, ACMatchClassification::MATCH));
  m.description_class.push_back(
      ACMatchClassification(runLength1, ACMatchClassification::NONE));

  NSAttributedString* decorated =
      AutocompletePopupViewMac::MatchText(m, font_, kLargeWidth);

  // Result contains the characters of the input.
  EXPECT_GT([decorated length], [contents length] + [description length]);
  EXPECT_TRUE([[decorated string] hasPrefix:contents]);
  EXPECT_TRUE([[decorated string] hasSuffix:description]);

  // Check that the description is a different color from the
  // contents.
  const NSUInteger descriptionLocation =
      [decorated length] - [description length];
  EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
                                  NSForegroundColorAttributeName),
            descriptionLocation);
  EXPECT_EQ(RunLengthForAttribute(decorated, descriptionLocation,
                                  NSForegroundColorAttributeName),
            [description length]);

  // Should have three font runs, not bold, bold, then not bold again.
  // The first run is the contents and the separator, the second run
  // is the first run of the description.
  EXPECT_EQ(RunLengthForAttribute(decorated, 0U,
                                  NSFontAttributeName), descriptionLocation);
  EXPECT_FALSE(RunHasFontTrait(decorated, 0U, NSBoldFontMask));

  EXPECT_EQ(RunLengthForAttribute(decorated, descriptionLocation,
                                  NSFontAttributeName), runLength1);
  EXPECT_TRUE(RunHasFontTrait(decorated, descriptionLocation, NSBoldFontMask));

  EXPECT_EQ(RunLengthForAttribute(decorated, descriptionLocation + runLength1,
                                  NSFontAttributeName), runLength2);
  EXPECT_FALSE(RunHasFontTrait(decorated, descriptionLocation + runLength1,
                               NSBoldFontMask));
}

TEST_F(AutocompletePopupViewMacTest, ElideString) {
  NSString* const contents = @"This is a test with long contents";
  const std::wstring wideContents(base::SysNSStringToWide(contents));

  const float kWide = 1000.0;
  const float kNarrow = 20.0;

  NSDictionary* attributes =
      [NSDictionary dictionaryWithObject:font_.GetNativeFont()
                                  forKey:NSFontAttributeName];
  scoped_nsobject<NSMutableAttributedString> as(
      [[NSMutableAttributedString alloc] initWithString:contents
                                             attributes:attributes]);

  // Nothing happens if the space is really wide.
  NSMutableAttributedString* ret =
      AutocompletePopupViewMac::ElideString(as, wideContents, font_, kWide);
  EXPECT_TRUE(ret == as);
  EXPECT_TRUE([[as string] isEqualToString:contents]);

  // When elided, result is the same as ElideText().
  ret = AutocompletePopupViewMac::ElideString(as, wideContents, font_, kNarrow);
  std::wstring elided(ElideText(wideContents, font_, kNarrow, false));
  EXPECT_TRUE(ret == as);
  EXPECT_FALSE([[as string] isEqualToString:contents]);
  EXPECT_TRUE([[as string] isEqualToString:base::SysWideToNSString(elided)]);

  // When elided, result is the same as ElideText().
  ret = AutocompletePopupViewMac::ElideString(as, wideContents, font_, 0.0);
  elided = ElideText(wideContents, font_, 0.0, false);
  EXPECT_TRUE(ret == as);
  EXPECT_FALSE([[as string] isEqualToString:contents]);
  EXPECT_TRUE([[as string] isEqualToString:base::SysWideToNSString(elided)]);
}

TEST_F(AutocompletePopupViewMacTest, MatchTextElide) {
  NSString* const contents = @"This is a test with long contents";
  NSString* const description = @"That was a test";
  // Match "long".
  const NSUInteger runLength1 = 20, runLength2 = 4, runLength3 = 9;
  // Make sure nobody messed up the inputs.
  EXPECT_EQ(runLength1 + runLength2 + runLength3, [contents length]);

  AutocompleteMatch m = MakeMatch(base::SysNSStringToWide(contents),
                                  base::SysNSStringToWide(description));

  // Push each run onto contents classifications.
  m.contents_class.push_back(
      ACMatchClassification(0, ACMatchClassification::NONE));
  m.contents_class.push_back(
      ACMatchClassification(runLength1, ACMatchClassification::MATCH));
  m.contents_class.push_back(
      ACMatchClassification(runLength1 + runLength2,
                            ACMatchClassification::URL));

  // Figure out the width of the contents.
  NSDictionary* attributes =
      [NSDictionary dictionaryWithObject:font_.GetNativeFont()
                                  forKey:NSFontAttributeName];
  const float contentsWidth = [contents sizeWithAttributes:attributes].width;

  // After accounting for the width of the image, this will force us
  // to elide the contents.
  float cellWidth = ceil(contentsWidth / 0.7);

  NSAttributedString* decorated =
      AutocompletePopupViewMac::MatchText(m, font_, cellWidth);

  // Results contain a prefix of the contents and all of description.
  NSString* commonPrefix =
      [[decorated string] commonPrefixWithString:contents options:0];
  EXPECT_GT([commonPrefix length], 0U);
  EXPECT_LT([commonPrefix length], [contents length]);
  EXPECT_TRUE([[decorated string] hasSuffix:description]);

  // At one point the code had a bug where elided text was being
  // marked up using pre-elided offsets, resulting in out-of-range
  // values being passed to NSAttributedString.  Push the ellipsis
  // through part of each run to verify that we don't continue to see
  // such things.
  while([commonPrefix length] > runLength1 - 3) {
    EXPECT_GT(cellWidth, 0.0);
    cellWidth -= 1.0;
    decorated = AutocompletePopupViewMac::MatchText(m, font_, cellWidth);
    commonPrefix =
        [[decorated string] commonPrefixWithString:contents options:0];
    ASSERT_GT([commonPrefix length], 0U);
  }
}

// TODO(shess): Test that
// AutocompletePopupViewMac::UpdatePopupAppearance() creates/destroys
// the popup according to result contents.  Test that the matrix gets
// the right number of results.  Test the contents of the cells for
// the right strings.  Icons?  Maybe, that seems harder to test.
// Styling seems almost impossible.

// TODO(shess): Test that AutocompletePopupViewMac::PaintUpdatesNow()
// updates the matrix selection.

// TODO(shess): Test that AutocompletePopupViewMac::AcceptInput()
// updates the model's selection from the matrix before returning.
// Could possibly test that via -select:.

// TODO(shess): Test that AutocompleteButtonCell returns the right
// background colors for on, highlighted, and neither.

// TODO(shess): Test that AutocompleteMatrixTarget can be initialized
// and then sends -select: to the view.

}  // namespace
