# Copyright 2015 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 unittest

import mock
import webapp2
import webtest

from dashboard import debug_alert
from dashboard import testing_common
from dashboard import utils
from dashboard.models import anomaly
from dashboard.models import anomaly_config
from dashboard.models import graph_data

_SAMPLE_SERIES = [
    (300, 60.06), (301, 60.36), (302, 61.76), (303, 60.06), (304, 61.24),
    (305, 60.65), (306, 55.61), (307, 61.88), (308, 61.51), (309, 59.58),
    (310, 71.79), (311, 71.97), (312, 71.63), (313, 67.16), (314, 70.91),
    (315, 73.40), (316, 71.00), (317, 69.45), (318, 67.16), (319, 66.05),
]


class DebugAlertTest(testing_common.TestCase):

  def setUp(self):
    super(DebugAlertTest, self).setUp()
    app = webapp2.WSGIApplication(
        [('/debug_alert', debug_alert.DebugAlertHandler)])
    self.testapp = webtest.TestApp(app)
    self.PatchDatastoreHooksRequest()

  def _AddSampleData(self):
    """Adds a Test and Row entities, and returns the Test key."""
    testing_common.AddTests(['M'], ['b'], {'suite': {'foo': {}}})
    test_path = 'M/b/suite/foo'
    rows_dict = {x: {'value': y} for x, y in _SAMPLE_SERIES}
    testing_common.AddRows(test_path, rows_dict)
    return utils.TestKey(test_path)

  def testGet_WithInvalidTestPath_ShowsFormAndError(self):
    response = self.testapp.get('/debug_alert?test_path=foo')
    self.assertIn('<form', response.body)
    self.assertIn('class="error"', response.body)

  def testGet_WithValidTestPath_ShowsChart(self):
    test_key = self._AddSampleData()
    test_path = utils.TestPath(test_key)
    response = self.testapp.get('/debug_alert?test_path=%s' % test_path)
    self.assertIn('id="plot"', response.body)

  def testPost_SameAsGet(self):
    # Post is the same as get for this endpoint.
    test_key = self._AddSampleData()
    test_path = utils.TestPath(test_key)
    get_response = self.testapp.get('/debug_alert?test_path=%s' % test_path)
    post_response = self.testapp.post('/debug_alert?test_path=%s' % test_path)
    self.assertEqual(get_response.body, post_response.body)

  def testGet_WithNoParameters_ShowsForm(self):
    response = self.testapp.get('/debug_alert')
    self.assertIn('<form', response.body)
    self.assertNotIn('id="plot"', response.body)

  def testGet_WithRevParameter_EmbedsCorrectRevisions(self):
    test_key = self._AddSampleData()
    test_path = utils.TestPath(test_key)
    response = self.testapp.get(
        '/debug_alert?test_path=%s&rev=%s&num_before=%s&num_after=%s' %
        (test_path, 305, 10, 5))
    self.assertEqual(
        [300, 301, 302, 303, 304, 305, 306, 307, 308, 309],
        self.GetEmbeddedVariable(response, 'LOOKUP'))

  def testGet_InvalidNumBeforeParameter_ShowsFormAndError(self):
    test_key = self._AddSampleData()
    test_path = utils.TestPath(test_key)
    response = self.testapp.get(
        '/debug_alert?test_path=%s&rev=%s&num_before=%s&num_after=%s' %
        (test_path, 305, 'foo', 5))
    self.assertIn('<form', response.body)
    self.assertIn('class="error"', response.body)
    self.assertNotIn('LOOKUP', response.body)

  def _AddAnomalyConfig(self, config_name, test_key, config_dict):
    """Adds a custom anomaly config which applies to one test."""
    anomaly_config_key = anomaly_config.AnomalyConfig(
        id=config_name,
        config=config_dict,
        patterns=[utils.TestPath(test_key)]).put()
    return anomaly_config_key

  @mock.patch.object(debug_alert, 'SimulateAlertProcessing')
  def testGet_TestHasOverriddenConfig_ConfigUsed(self, simulate_mock):
    test_key = self._AddSampleData()
    # Add a config which applies to the test. The test is updated upon put.
    self._AddAnomalyConfig('X', test_key, {'min_absolute_change': 10})
    test_key.get().put()
    response = self.testapp.get(
        '/debug_alert?test_path=%s' % utils.TestPath(test_key))
    # The custom config should be used when simulating alert processing.
    simulate_mock.assert_called_once_with(mock.ANY, min_absolute_change=10)
    # The config JSON should also be put into the form on the page.
    self.assertIn('"min_absolute_change": 10', response.body)

  @mock.patch.object(debug_alert, 'SimulateAlertProcessing')
  def testGet_WithValidCustomConfig_ConfigUsed(self, simulate_mock):
    test_key = self._AddSampleData()
    response = self.testapp.get(
        '/debug_alert?test_path=%s&config=%s' %
        (utils.TestPath(test_key),
         '{"min_relative_change":0.75}'))
    # The custom config should be used when simulating alert processing.
    simulate_mock.assert_called_once_with(mock.ANY, min_relative_change=0.75)
    # The config JSON should also be put into the form on the page.
    self.assertIn('"min_relative_change": 0.75', response.body)

  @mock.patch.object(debug_alert, 'SimulateAlertProcessing')
  def testGet_WithBogusParameterNames_ParameterIgnored(self, simulate_mock):
    test_key = self._AddSampleData()
    response = self.testapp.get(
        '/debug_alert?test_path=%s&config=%s' %
        (utils.TestPath(test_key), '{"foo":0.75}'))
    simulate_mock.assert_called_once_with(mock.ANY)
    self.assertNotIn('"foo"', response.body)

  def testGet_WithInvalidCustomConfig_ErrorShown(self):
    test_key = self._AddSampleData()
    response = self.testapp.get(
        '/debug_alert?test_path=%s&config=%s' %
        (utils.TestPath(test_key), 'not valid json'))
    # The error message should be on the page; JS constants should not be.
    self.assertIn('Invalid JSON', response.body)
    self.assertNotIn('LOOKUP', response.body)

  def testGet_WithStoredAnomalies_ShowsStoredAnomalies(self):
    test_key = self._AddSampleData()
    anomaly.Anomaly(
        test=test_key, start_revision=309, end_revision=310,
        median_before_anomaly=60, median_after_anomaly=70,
        bug_id=12345).put()
    response = self.testapp.get(
        '/debug_alert?test_path=%s' % utils.TestPath(test_key))
    # Information about the stored anomaly should be somewhere on the page.
    self.assertIn('12345', response.body)

  def testFetchLatestRows(self):
    test_key = self._AddSampleData()
    rows = debug_alert._FetchLatestRows(test_key.get(), 4)
    revisions = [r.revision for r in rows]
    self.assertEqual([316, 317, 318, 319], revisions)

  def testFetchAroundRev(self):
    test_key = self._AddSampleData()
    rows = debug_alert._FetchRowsAroundRev(test_key.get(), 310, 5, 8)
    revisions = [r.revision for r in rows]
    self.assertEqual(
        [305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317],
        revisions)

  def testFetchRowsAroundRev_NotAllRowsAvailable(self):
    test_key = self._AddSampleData()
    rows = debug_alert._FetchRowsAroundRev(test_key.get(), 310, 100, 100)
    # There are only 20 rows in the sample data, so only 20 can be fetched.
    self.assertEqual(20, len(rows))

  def testChartSeries(self):
    test_key = self._AddSampleData()
    rows = debug_alert._FetchRowsAroundRev(test_key.get(), 310, 5, 5)
    # The indexes used in the chart series should match those in the lookup.
    self.assertEqual(
        [(0, 60.65), (1, 55.61), (2, 61.88), (3, 61.51), (4, 59.58),
         (5, 71.79), (6, 71.97), (7, 71.63), (8, 67.16), (9, 70.91)],
        debug_alert._ChartSeries(rows))

  def testRevisionList(self):
    test_key = self._AddSampleData()
    rows = debug_alert._FetchRowsAroundRev(test_key.get(), 310, 5, 5)
    # The lookup dict maps indexes to x-values in the input series.
    self.assertEqual(
        [305, 306, 307, 308, 309, 310, 311, 312, 313, 314],
        debug_alert._RevisionList(rows))

  def testCsvUrl_RowsGiven_AllParamsSpecified(self):
    self._AddSampleData()
    rows = graph_data.Row.query().fetch(limit=20)
    self.assertEqual(
        '/graph_csv?test_path=M%2Fb%2Fsuite%2Ffoo&num_points=20&rev=319',