// <snippet module="ui/core/dependency-observable" title="dependency-observable">
// # DependencyObservable
// DependencyObservable is a fundamental building block in the NativeScript visual tree (DOM).
// It provides enhanced property system allowing for various value modifiers on a per property basis.
// This enables cascading values - e.g. default vs. inherited vs. local.
// Load the module using the following code:
// ``` JavaScript
import dependencyObservableModule = require("ui/core/dependency-observable");
// ```
// </snippet>

import TKUnit = require("../TKUnit");
import types = require("utils/types");
import observableModule = require("data/observable");

var currentChangeData: dependencyObservableModule.PropertyChangeData;
var propertyNameCount = 0;

function generatePropertyName() {
    var name = "test" + propertyNameCount++;
    return name;
}

function onTestPropertyChanged(data: dependencyObservableModule.PropertyChangeData) {
    currentChangeData = data;
}

function validateDOChangeData(obj: TestDO, property: dependencyObservableModule.Property, oldValue: any, newValue: any) {
    TKUnit.assert(types.isDefined(currentChangeData), "metadata.onValueChanged function not called.");
    TKUnit.assert(currentChangeData.object === obj, "metadata.onValueChanged function called with wrong object member.");
    TKUnit.assert(currentChangeData.property === property, "metadata.onValueChanged method called with wrong property member");
    TKUnit.assert(currentChangeData.newValue === newValue, "metadata.onValueChanged method called with wrong newValue member");
    TKUnit.assert(currentChangeData.oldValue === oldValue, "metadata.onValueChanged method called with wrong oldValue member");
    TKUnit.assert(currentChangeData.eventName === observableModule.Observable.propertyChangeEvent, "metadata.onValueChanged method called with wrong eventName member");
}

function validateChangeData(data: observableModule.PropertyChangeData, obj: observableModule.Observable, propertyName: string, newValue: any) {
    TKUnit.assert(types.isDefined(data), "DependencyObservable.on for propertyChange not called.");
    TKUnit.assert(data.object === obj, "DependencyObservable.on called with wrong object member.");
    TKUnit.assert(data.propertyName === propertyName, "DependencyObservable.on called with wrong propertyName member");
    TKUnit.assert(data.value === newValue, "DependencyObservable.on called with wrong value member");
    TKUnit.assert(data.eventName === observableModule.Observable.propertyChangeEvent, "DependencyObservable.on method called with wrong eventName member");
}

// <snippet module="ui/core/dependency-observable" title="dependency-observable">
// ### Creating a Property
// The property backing mechanism in each DependencyObservable instance is implemented via the Property class.
// Basically you create a new Property for a class (type):
// ``` JavaScript
var testProperty = new dependencyObservableModule.Property(
    "test",
    "TestDO",
    new dependencyObservableModule.PropertyMetadata(
        false,
        dependencyObservableModule.PropertyMetadataSettings.None /*0*/,
        onTestPropertyChanged
        )
    );
// ```
// </snippet>

var testProperty1 = new dependencyObservableModule.Property(
    "test1",
    "TestDO",
    new dependencyObservableModule.PropertyMetadata(
        0,
        dependencyObservableModule.PropertyMetadataSettings.None /*0*/,
        onTestPropertyChanged
        )
    );

function validateProperty(value: any): boolean {
    return types.isNumber(value) && value >= 0;
}
var testPropertyWithValidation = new dependencyObservableModule.Property(
    "testWithValidation",
    "TestDO",
    new dependencyObservableModule.PropertyMetadata(
        0,
        dependencyObservableModule.PropertyMetadataSettings.None /*0*/,
        onTestPropertyChanged,
        validateProperty
        )
    );

class TestDO extends dependencyObservableModule.DependencyObservable {
    get test(): boolean {
        return this._getValue(testProperty);
    }
    set test(value: boolean) {
        this._setValue(testProperty, value);
    }

    get test1(): number {
        return this._getValue(testProperty1);
    }
    set test1(value: number) {
        this._setValue(testProperty1, value);
    }

    get testWithValidation(): number {
        return this._getValue(testPropertyWithValidation);
    }
    set testWithValidation(value: number) {
        this._setValue(testPropertyWithValidation, value);
    }

    public _onPropertyChanged(property: dependencyObservableModule.Property, oldValue: any, newValue: any) {
        super._onPropertyChanged(property, oldValue, newValue);
    }
}

export function test_DO_Members_AreDefined() {
    var obj = new dependencyObservableModule.DependencyObservable();
    TKUnit.assert(types.isDefined(obj._setValue), "DependencyObservable._setValue is undefined.");
    TKUnit.assert(types.isDefined(obj._getValue), "DependencyObservable._getValue is undefined.");
    TKUnit.assert(types.isDefined(obj._getValueSource), "DependencyObservable._getValueSource is undefined.");
    TKUnit.assert(types.isDefined(obj._resetValue), "DependencyObservable._resetValue is undefined.");
    TKUnit.assert(types.isDefined(obj._onPropertyChanged), "DependencyObservable._resetValue is undefined.");
    TKUnit.assert(types.isDefined(obj._eachSetProperty), "DependencyObservable._resetValue is undefined.");
}

export function test_NewProperty_WillThrow_If_Name_IsInvalid() {
    var thrown = false;
    try {
        /* tslint:disable:no-unused-variable */
        var p = new dependencyObservableModule.Property("", "", undefined);
        /* tslint:enable:no-unused-variable */
    }
    catch (e) {
        thrown = true;
        TKUnit.assert(e.message === "Name should not be null or empty string.", "Invalid error message");
    }

    TKUnit.assert(thrown, "Should not create a Property with invalid name.");
}

export function test_NewProperty_WillThrow_If_OwnerType_IsInvalid() {
    var thrown = false;
    try {
        /* tslint:disable:no-unused-variable */
        var p = new dependencyObservableModule.Property(generatePropertyName(), "", undefined);
        /* tslint:enable:no-unused-variable */
    }
    catch (e) {
        thrown = true;
        TKUnit.assert(e.message === "OwnerType should not be null or empty string.", "Invalid error message");
    }

    TKUnit.assert(thrown, "Should not create a Property with invalid owner type.");
}

export function test_NewProperty_WillThrow_If_Metadata_IsInvalid() {
    var thrown = false;
    var p;

    try {
        // check for undefined
        p = new dependencyObservableModule.Property(generatePropertyName(), "testOwner", undefined);
    }
    catch (e) {
        thrown = true;
        TKUnit.assert(e.message === "Expected valid PropertyMetadata instance.", "Invalid error message");
    }

    TKUnit.assert(thrown, "Should not create a Property with undefined metadata.");

    thrown = false;
    try {
        // check for invalid type
        var metadata: any = {};
        p = new dependencyObservableModule.Property(generatePropertyName(), "testOwner", metadata);
    }
    catch (e) {
        thrown = true;
        TKUnit.assert(e.message === "Expected valid PropertyMetadata instance.", "Invalid error message");
    }

    TKUnit.assert(thrown, "Should not create a Property with invalid metadata type.");
}

export function test_NewProperty_WillThrow_If_Key_AlreadyRegistered() {
    var thrown = false;
    try {
        /* tslint:disable:no-unused-variable */
        var p = new dependencyObservableModule.Property("test", "testOwner", new dependencyObservableModule.PropertyMetadata(0));
        var p1 = new dependencyObservableModule.Property("test", "testOwner", new dependencyObservableModule.PropertyMetadata(0));
        /* tslint:enable:no-unused-variable */
    }
    catch (e) {
        thrown = true;
        TKUnit.assert(e.message === "Property test already registered for type testOwner.", "Invalid error message");
    }

    TKUnit.assert(thrown, "Should not create a Property with invalid name.");
}

export function test_Property_Id_IsUnique() {
    var p = new dependencyObservableModule.Property(generatePropertyName(), "testOwner", new dependencyObservableModule.PropertyMetadata(0));
    var p1 = new dependencyObservableModule.Property(generatePropertyName(), "testOwner", new dependencyObservableModule.PropertyMetadata(0));

    TKUnit.assert(p.id !== p1.id, "Property.id should be unique.");
}

export function test_PropertyMetadataSettings_HasValid_Memberes() {
    TKUnit.assert(dependencyObservableModule.PropertyMetadataSettings.None === 0, "PropertyMetadataSettings.None not defined correctly.");
    TKUnit.assert(dependencyObservableModule.PropertyMetadataSettings.AffectsLayout === 1, "PropertyMetadataSettings.AffectsLayout not defined correctly.");
    TKUnit.assert(dependencyObservableModule.PropertyMetadataSettings.AffectsStyle === 2, "PropertyMetadataSettings.AffectsStyle not defined correctly.");
    TKUnit.assert(dependencyObservableModule.PropertyMetadataSettings.Inheritable === 4, "PropertyMetadataSettings.Inheritable not defined correctly.");
}

export function test_ValueSource_HasValid_Memberes() {
    TKUnit.assert(dependencyObservableModule.ValueSource.Default === 0, "ValueSource.None not defined correctly.");
    TKUnit.assert(dependencyObservableModule.ValueSource.Inherited === 1, "ValueSource.Inherited not defined correctly.");
    TKUnit.assert(dependencyObservableModule.ValueSource.Css === 2, "ValueSource.Css not defined correctly.");
    TKUnit.assert(dependencyObservableModule.ValueSource.Local === 3, "ValueSource.Local not defined correctly.");
    TKUnit.assert(dependencyObservableModule.ValueSource.VisualState === 4, "ValueSource.VisualState not defined correctly.");
}

export function test_PropertyMetadata_Options() {
    var options =
        dependencyObservableModule.PropertyMetadataSettings.AffectsLayout |
        dependencyObservableModule.PropertyMetadataSettings.AffectsStyle |
        dependencyObservableModule.PropertyMetadataSettings.Inheritable;

    var p = new dependencyObservableModule.Property(generatePropertyName(), "testOwner", new dependencyObservableModule.PropertyMetadata(0, options));

    TKUnit.assert(p.affectsLayout === true, "Property.affectsLayout not working as expected.");
    TKUnit.assert(p.affectsStyle === true, "Property.affectsStyle not working as expected.");
    TKUnit.assert(p.inheritable === true, "Property.inheritable not working as expected.");
    TKUnit.assert(p.metadata.options === options, "PropertyMetadata.options not properly set.");
}

export function test_PropertyEntry_effectiveValue() {
    var entry = new dependencyObservableModule.PropertyEntry();

    // default value
    TKUnit.assertEqual(entry.effectiveValue, undefined, "effectiveValue should be undefined.");
    TKUnit.assertEqual(entry.valueSource, dependencyObservableModule.ValueSource.Default, "ValueSource.Default not working as expected.");

    // local value
    entry.localValue = 10;
    entry.resetValue();
    
    // default value
    TKUnit.assertEqual(entry.effectiveValue, undefined, "ValueSource.Default not working as expected.");
    TKUnit.assertEqual(entry.valueSource, dependencyObservableModule.ValueSource.Default, "ValueSource.Default not working as expected.");
}

export function test_DependencyObservable_get_set_AreOverriden() {
    var dO = new TestDO();

    dO.test = true;
    TKUnit.assert(dO.get("test") === true, "DependencyObservable should override Observable.get");

    dO.set("test", false);
    TKUnit.assert(dO.test === false, "DependencyObservable should override Observable.set");
}

export function test_DependencyObservable_getValue_setValue() {
    var dO = new TestDO();

    // implicitly use ValueSource.Local when not specified
    dO._setValue(testProperty1, 5);
    TKUnit.assertEqual(dO.test1, 5, "expected true after _setValue.");
    TKUnit.assertEqual(dO._getValueSource(testProperty1), dependencyObservableModule.ValueSource.Local, "_setValue without third parameter should use ValueSource.Local");

    dO._setValue(testProperty1, 10, dependencyObservableModule.ValueSource.Inherited);
    TKUnit.assertEqual(dO.test1, 5, "ValueSource.Local should have higher priority than Inherited.");

    // revert to default value
    dO._resetValue(testProperty1);
    TKUnit.assertEqual(dO.test1, 10, "_resetValue should revert to inheritedValue");

    // test the ValueSource parameter
    dO._setValue(testProperty1, 10, dependencyObservableModule.ValueSource.Inherited);
    TKUnit.assertEqual(dO.test1, 10, "_setValue with three parameters not working as expected.");
    TKUnit.assertEqual(dO._getValueSource(testProperty1), dependencyObservableModule.ValueSource.Inherited, "expected ValueSource.Inherited");

    dO._setValue(testProperty1, 20, dependencyObservableModule.ValueSource.Local);
    TKUnit.assertEqual(dO.test1, 20, "_setValue with three parameters not working as expected.");
    TKUnit.assertEqual(dO._getValueSource(testProperty1), dependencyObservableModule.ValueSource.Local, "expected ValueSource.Local");

    // reset the Local value and verify the effectiveValue will be reverted to the Inherited one.
    dO._resetValue(testProperty1, dependencyObservableModule.ValueSource.Local);
    TKUnit.assertEqual(dO.test1, 10, "_resetValue for ValueSource not working as expected.");
    TKUnit.assertEqual(dO._getValueSource(testProperty1), dependencyObservableModule.ValueSource.Inherited, "expected ValueSource.Inherited");
}

export function test_PropertyMetadata_onValueChanged_Callback_IsInvoked() {
    currentChangeData = undefined;

    var dO = new TestDO();
    dO.test = true;

    validateDOChangeData(dO, testProperty, false, true);

    currentChangeData = undefined;
    var dO1 = new TestDO();
    dO1.test1 = 10;

    validateDOChangeData(dO1, testProperty1, 0, 10);

    // reset value
    currentChangeData = undefined;
    dO1._resetValue(testProperty1);
    validateDOChangeData(dO1, testProperty1, 10, 0);

    currentChangeData = undefined;
    dO1._setValue(testProperty1, 10, dependencyObservableModule.ValueSource.Inherited);
    validateDOChangeData(dO1, testProperty1, 0, 10);

    currentChangeData = undefined;
    dO1._setValue(testProperty1, 20, dependencyObservableModule.ValueSource.Local);
    validateDOChangeData(dO1, testProperty1, 10, 20);

    currentChangeData = undefined;
    dO1._resetValue(testProperty1, dependencyObservableModule.ValueSource.Local);
    validateDOChangeData(dO1, testProperty1, 20, 10);
}

export function test_Observable_on_propertyChange_IsRaised() {
    var dO1 = new TestDO();

    var changeData: observableModule.PropertyChangeData;
    dO1.on(observableModule.Observable.propertyChangeEvent, function (data: observableModule.PropertyChangeData) {
        changeData = data;
    });

    dO1.test1 = 10;
    validateChangeData(changeData, dO1, "test1", 10);

    // reset value
    changeData = undefined;
    dO1._resetValue(testProperty1);
    validateChangeData(changeData, dO1, "test1", 0);

    changeData = undefined;
    dO1._setValue(testProperty1, 10, dependencyObservableModule.ValueSource.Inherited);
    validateChangeData(changeData, dO1, "test1", 10);

    changeData = undefined;
    dO1._setValue(testProperty1, 20, dependencyObservableModule.ValueSource.Local);
    validateChangeData(changeData, dO1, "test1", 20);

    changeData = undefined;
    dO1._resetValue(testProperty1, dependencyObservableModule.ValueSource.Local);
    validateChangeData(changeData, dO1, "test1", 10);
}

export function test_setValue_WillThrow_If_ValueIsInvalid() {
    var dO = new TestDO();

    TKUnit.assertThrows(
        () => {
            dO.testWithValidation = -1;
        },
        "Set invalid value should throw",
        "Invalid value -1 for property testWithValidation");

}

export function test_setValue_WillNotThrow_If_ValueIsValid() {
    var dO = new TestDO();
    // This should not throw.
    dO.testWithValidation = 1;
}