define([
    'aui',
    'jquery',
    'lodash',
    'Squire',
    'test-helper',
    'bitbucket/internal/feature/tasks/model/task',
    'bitbucket/internal/util/function'
], function (
    AJS,
    $,
    _,
    Squire,
    testHelper,
    Task,
    fn
) {

    'use strict';

    // Some dummy users to simplify our stubbing
    var currentUser = { id: 1, name: 'user'};
    var reviewerUser = { id: 2, name: 'reviewer-user'};
    var prAuthorUser = { id: 3, name: 'pr-author-user'};
    var adminUser = { id: 4, name: 'admin-user', isAdmin: true};
    var otherUser = { id: 5, name: 'other-user'};
    var taskAuthorUser = { id: 6, name: 'task-author-user'};

    var mockPageState = {
        pullRequest: {
            id: 10,
            author: { user: currentUser },
            reviewers: [ { user: currentUser }],
            participants: []
        },
        repository: {
            id: 20
        },
        currentUser: currentUser
    };

    var originalMockPageState = _.cloneDeep(mockPageState);

    var $fixture;

    var task = new Task({
        id:            1,
        anchor:        {},
        state:         'OPEN',
        author:        taskAuthorUser,
        createdDate: Date.now(),
        // optional fields
        text: 'This is task text',
        html: 'This is task text',
        pullRequestId: 10,
        repositoryId: 20
    });

    var injector = new Squire();

    var mockAnalytics = {
        add: sinon.spy()
    };

    var mockEvents = new testHelper.SimpleEvents();

    var mockEventsApi = {
        chain: fn.constant(mockEvents)
    };

    // all task-analytics uses the collection for is to trigger events
    // on it, so we can get away with just simplyfing things by returing the
    // mock events object as the collection.
    var mockTaskCollection = {
        getCollection: fn.constant(mockEvents)
    };

    injector.setDefaultDeps(['aui',
        'bitbucket/util/state',
        'bitbucket/internal/feature/tasks/task-analytics']);
    injector.mock('bitbucket/util/events', mockEventsApi);
    injector.mock('bitbucket/internal/feature/tasks/model/task-collection', mockTaskCollection);
    injector.mock('bitbucket/internal/util/analytics', mockAnalytics);
    injector.mock('bitbucket/util/state', testHelper.mockPageStateApi(mockPageState));
    injector.mock('aui', AJS);

    module('tasks-analytics', {
        setup: function () {
            $fixture = $('#qunit-fixture');
            this.task = task;
            // insert a tab by default for overview.
            insertTab('overview');
        },
        teardown: function () {
            removeDialog();
            removeTab();
            $fixture = null;
            // make sure the page state gets reset between tests
            mockPageState = _.cloneDeep(originalMockPageState);
        }
    });

    function setPRAuthor() {
        mockPageState.pullRequest.author.user = prAuthorUser;
    }

    /**
     * Set a user as a PR reviewer
     * @param user
     */
    function setPRReviewer(user) {
        mockPageState.pullRequest.reviewers = [ { user: user }];
    }

    /**
     * Helper to set the page state to indicate that the current user is the PR author
     */
    function executeWithPRAuthor() {
        setPRAuthor(prAuthorUser);
        mockPageState.currentUser = prAuthorUser;
    }

    /**
     * Helper to set the page state to indicate that the current user is a PR Reviewer
     */
    function executeWithPRReviewer() {
        setPRReviewer(reviewerUser);
        mockPageState.currentUser = reviewerUser;
    }

    /**
     * Helper to set the page state to indicate that the current user is an "other" user
     */
    function executeWithOther() {
        setPRAuthor(prAuthorUser);
        setPRReviewer(reviewerUser);
        mockPageState.currentUser = otherUser;
    }

    /**
     * Helper to set the page state to indicate that the current user is the Task author
     */
    function executeWithTaskAuthor() {
        setPRAuthor(otherUser);
        mockPageState.currentUser = taskAuthorUser;
    }

    /**
     * Helper to set the page state to indicate that the current user is an admin
     */
    function executeWithAdmin() {
        setPRAuthor(otherUser);
        mockPageState.currentUser = adminUser;
    }

    function getAnalyticsEvent(name) {
        return 'pullRequest.' + name;
    }

    // These location setters help us to run every single event as if it were triggered in a particular location
    var locationSetters = {
        'activity-stream': insertTab.bind(null, 'overview'),
        'diff-tab': insertTab.bind(null, 'diff'),
        'commits-tab': insertTab.bind(null, 'commits'),
        'task-list': insertDialog.bind(null, false)
    };


    function doAnalyticsTest(name, testdataSet) {
        // an async test so we can execute the events and build up
        // 2 data sets with the expected event data and with the actual analytics event data
        injector.test(name, function (assert, AJS, pageState, taskAnalytics) {
            var self = this;
            var done = assert.async();
            // Trigger the events in the various locations to test that the 'source' is passed along to the event
            _.forEach(locationSetters, function (locationSetter, location) {
                testdataSet.forEach(function (data) {

                    // merge some additional things on to the event data or the task that we're sending through.
                    var eventData = _.merge({}, {task: self.task.toJSON()}, data.additionalEventData);
                    self.task = _.merge(self.task, data.additionalTaskData);

                    // our expected properties that should exist on the analytics event
                    var expectedData = {
                        name: data.analyticsEvent,
                        data: {
                            "pullRequest.id": self.task.pullRequestId,
                            "repository.id": self.task.repositoryId,
                            source: location
                        }
                    };
                    if (data.onTask) {
                        expectedData.data['task.id'] = self.task.id;
                        expectedData.data['task.text.length'] = self.task.getText().length;
                    }

                    // set the page state
                    data.stateSetter();

                    // even though the pageState is mocked, it is different for each run of the
                    // loop - so we need to force set it to the values expected for this test.
                    var mockPageStateApi = testHelper.mockPageStateApi(mockPageState);

                    pageState.getCurrentUser = mockPageStateApi.getCurrentUser;
                    pageState.getPullRequest = mockPageStateApi.getPullRequest;
                    pageState.getRepository = mockPageStateApi.getRepository;

                    locationSetter();
                    taskAnalytics.init();

                    if (data.taskEvent) {
                        mockEvents.trigger('bitbucket.internal.feature.' + data.taskEvent, eventData);
                    }
                    if (data.collectionEvent) {
                        mockEvents.trigger(data.collectionEvent, self.task);
                    }

                    assert.ok(mockAnalytics.add.calledOnce);
                    assert.ok(mockAnalytics.add.calledWithMatch(sinon.match.string, sinon.match.object));

                    var actual = {
                        name: mockAnalytics.add.args[0][0],
                        data: mockAnalytics.add.args[0][1]
                    };

                    assert.equal(actual.name, expectedData.name);

                    // create an expected data object by merging 'expected' on to 'actual' because
                    // 'expected' only contains the attributes we are interested in testing.
                    var expected = _.merge({}, mockAnalytics.add.args[0][1], expectedData.data);
                    assert.deepEqual(actual.data, expected, actual.name + ' [' + actual.data.source + '] fired with expected data');

                    mockAnalytics.add.reset();
                    mockEvents.reset();
                });
            });
            done();
        });
    }

    // N.B.
    // - Use "collectionEvent" to signify that the event you're triggering happens on the TaskCollection
    // - Use "taskEvent" to signify that is is a global event (i.e. events.trigger())
    // - Add a flag "onTask" to signify that the event is triggered on a task.
    //   Those events should also carry task specific information with them.

    doAnalyticsTest('Create task analytics events', [
        { collectionEvent: 'change:id', analyticsEvent: getAnalyticsEvent('task.created.byPRAuthor'), onTask: true, stateSetter: executeWithPRAuthor },
        { collectionEvent: 'change:id', analyticsEvent: getAnalyticsEvent('task.created.byReviewer'), onTask: true, stateSetter: executeWithPRReviewer },
        { collectionEvent: 'change:id', analyticsEvent: getAnalyticsEvent('task.created.byOther'),    onTask: true, stateSetter: executeWithOther }
    ]);

    doAnalyticsTest('Create prefilled task analytics events', [
        { collectionEvent: 'change:id', analyticsEvent: getAnalyticsEvent('task.created.prefilled.byPRAuthor'), onTask: true, additionalTaskData: { prefilled: true }, stateSetter: executeWithPRAuthor },
        { collectionEvent: 'change:id', analyticsEvent: getAnalyticsEvent('task.created.prefilled.byReviewer'), onTask: true, additionalTaskData: { prefilled: true }, stateSetter: executeWithPRReviewer },
        { collectionEvent: 'change:id', analyticsEvent: getAnalyticsEvent('task.created.prefilled.byOther'),    onTask: true, additionalTaskData: { prefilled: true }, stateSetter: executeWithOther }
    ]);

    doAnalyticsTest('Delete task analytics events', [
        { collectionEvent: 'destroy', analyticsEvent: getAnalyticsEvent('task.deleted.byPRAuthor'),   onTask: true, stateSetter: executeWithPRAuthor },
        { collectionEvent: 'destroy', analyticsEvent: getAnalyticsEvent('task.deleted.byTaskAuthor'), onTask: true, stateSetter: executeWithTaskAuthor },
        { collectionEvent: 'destroy', analyticsEvent: getAnalyticsEvent('task.deleted.byAdmin'),      onTask: true, stateSetter: executeWithAdmin }
    ]);

    doAnalyticsTest('Edit task analytics events', [
        { collectionEvent: 'change:text', analyticsEvent: getAnalyticsEvent('task.edited.byPRAuthor'),   onTask: true, stateSetter: executeWithPRAuthor },
        { collectionEvent: 'change:text', analyticsEvent: getAnalyticsEvent('task.edited.byTaskAuthor'), onTask: true, stateSetter: executeWithTaskAuthor },
        { collectionEvent: 'change:text', analyticsEvent: getAnalyticsEvent('task.edited.byAdmin'),      onTask: true, stateSetter: executeWithAdmin }
    ]);

    doAnalyticsTest('Resolve analytics events', [
        { taskEvent: 'pull-request-tasks.resolved', analyticsEvent: getAnalyticsEvent('task.resolved.byPRAuthor'),   onTask: true, stateSetter: executeWithPRAuthor },
        { taskEvent: 'pull-request-tasks.resolved', analyticsEvent: getAnalyticsEvent('task.resolved.byOther'),      onTask: true, stateSetter: executeWithPRReviewer },
        { taskEvent: 'pull-request-tasks.resolved', analyticsEvent: getAnalyticsEvent('task.resolved.byOther'),      onTask: true, stateSetter: executeWithOther },
        { taskEvent: 'pull-request-tasks.resolved', analyticsEvent: getAnalyticsEvent('task.resolved.byTaskAuthor'), onTask: true, stateSetter: executeWithTaskAuthor }
    ]);

    doAnalyticsTest('Reopen analytics events', [
        { taskEvent: 'pull-request-tasks.reopened', analyticsEvent: getAnalyticsEvent('task.reopened.byPRAuthor'),   onTask: true, stateSetter: executeWithPRAuthor },
        { taskEvent: 'pull-request-tasks.reopened', analyticsEvent: getAnalyticsEvent('task.reopened.byOther'),      onTask: true, stateSetter: executeWithPRReviewer },
        { taskEvent: 'pull-request-tasks.reopened', analyticsEvent: getAnalyticsEvent('task.reopened.byOther'),      onTask: true, stateSetter: executeWithOther },
        { taskEvent: 'pull-request-tasks.reopened', analyticsEvent: getAnalyticsEvent('task.reopened.byTaskAuthor'), onTask: true, stateSetter: executeWithTaskAuthor }
    ]);

    doAnalyticsTest('Open task list analytics events', [
        { taskEvent: 'tasks.list.opened', analyticsEvent: getAnalyticsEvent('task.list.opened.byPRAuthor'), stateSetter: executeWithPRAuthor },
        { taskEvent: 'tasks.list.opened', analyticsEvent: getAnalyticsEvent('task.list.opened.byReviewer'), stateSetter: executeWithPRReviewer},
        { taskEvent: 'tasks.list.opened', analyticsEvent: getAnalyticsEvent('task.list.opened.byOther'), stateSetter: executeWithOther }
    ]);

    doAnalyticsTest('Close task list analytics events', [
        { taskEvent: 'tasks.list.closed', analyticsEvent: getAnalyticsEvent('task.list.closed.byPRAuthor'), stateSetter: executeWithPRAuthor },
        { taskEvent: 'tasks.list.closed', analyticsEvent: getAnalyticsEvent('task.list.closed.byReviewer'), stateSetter: executeWithPRReviewer},
        { taskEvent: 'tasks.list.closed', analyticsEvent: getAnalyticsEvent('task.list.closed.byOther'), stateSetter: executeWithOther }
    ]);

    doAnalyticsTest('Expand task list analytics events', [
        { taskEvent: 'tasks.list.expand', analyticsEvent: getAnalyticsEvent('task.list.expandTask.byPRAuthor'), additionalEventData: { model: task }, stateSetter: executeWithPRAuthor },
        { taskEvent: 'tasks.list.expand', analyticsEvent: getAnalyticsEvent('task.list.expandTask.byReviewer'), additionalEventData: { model: task }, stateSetter: executeWithPRReviewer},
        { taskEvent: 'tasks.list.expand', analyticsEvent: getAnalyticsEvent('task.list.expandTask.byOther'),    additionalEventData: { model: task }, stateSetter: executeWithOther }
    ]);

    doAnalyticsTest('Go to task from task list analytics events', [
        { taskEvent: 'tasks.visit-task', analyticsEvent: getAnalyticsEvent('task.list.gotoTask.byPRAuthor'), stateSetter: executeWithPRAuthor },
        { taskEvent: 'tasks.visit-task', analyticsEvent: getAnalyticsEvent('task.list.gotoTask.byReviewer'), stateSetter: executeWithPRReviewer},
        { taskEvent: 'tasks.visit-task', analyticsEvent: getAnalyticsEvent('task.list.gotoTask.byOther'), stateSetter: executeWithOther }
    ]);

    doAnalyticsTest('Go to file from task list analytics events', [
        { taskEvent: 'tasks.visit-task', analyticsEvent: getAnalyticsEvent('task.list.gotoFile.byPRAuthor'), additionalEventData: { location: 'diff' }, stateSetter: executeWithPRAuthor },
        { taskEvent: 'tasks.visit-task', analyticsEvent: getAnalyticsEvent('task.list.gotoFile.byReviewer'), additionalEventData: { location: 'diff' }, stateSetter: executeWithPRReviewer},
        { taskEvent: 'tasks.visit-task', analyticsEvent: getAnalyticsEvent('task.list.gotoFile.byOther'),    additionalEventData: { location: 'diff' }, stateSetter: executeWithOther }
    ]);

    // Some DOM helpers
    /**
     * Insert a dialog
     * @param {boolean} hidden
     */
    function insertDialog(hidden) {
        removeDialog();
        $fixture.append('<div id="task-list-dialog" aria-hidden="' + hidden + '"/>');
    }

    function removeDialog() {
        $fixture.find('#task-list-dialog').remove();
    }

    /**
     * Insert an 'active tab' with a given module key
     * @param {string} module
     */
    function insertTab(module) {
        removeTab(); // remove any tabs that may already exist on the page
        removeDialog(); // remove the dialog because we want to enforce being on a particular tab
        $fixture.append('<div id="tab" class="active-tab" data-module-key="' + module + '"/>');
    }

    function removeTab() {
        $fixture.find('#tab').remove();
    }
});
