// Copyright 2023 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
//    list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
//    contributors may be used to endorse or promote products derived from
//    this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include <algorithm>
#include <vector>

#include "src/dawn/tests/unittests/validation/ValidationTest.h"
#include "src/dawn/utils/WGPUHelpers.h"
#include "src/dawn/utils/WireHelper.h"
#include "src/utils/compiler.h"

namespace dawn {
namespace {

class WGSLFeatureValidationTest : public ValidationTest {
  protected:
    struct InstanceSpec {
        bool useTestingFeatures = true;
        bool allowUnsafeAPIs = false;
        bool exposeExperimental = false;
        std::vector<const char*> blocklist = {};
    };

    // Override the default SetUp to do nothing since the tests will call SetUp(InstanceSpec)
    // explicitly each time.
    void SetUp() override {}
    void SetUp(InstanceSpec spec) {
        // The blocklist that will be shared between both the native and wire descriptors.
        wgpu::DawnWGSLBlocklist blocklist;
        blocklist.blocklistedFeatureCount = spec.blocklist.size();
        blocklist.blocklistedFeatures = spec.blocklist.data();

        // Build the native instance descriptor.
        std::vector<const char*> enabledToggles;
        if (spec.useTestingFeatures) {
            enabledToggles.push_back("expose_wgsl_testing_features");
        }
        if (spec.allowUnsafeAPIs) {
            enabledToggles.push_back("allow_unsafe_apis");
        }
        if (spec.exposeExperimental) {
            enabledToggles.push_back("expose_wgsl_experimental_features");
        }

        wgpu::DawnTogglesDescriptor togglesDesc;
        togglesDesc.nextInChain = &blocklist;
        togglesDesc.enabledToggleCount = enabledToggles.size();
        togglesDesc.enabledToggles = enabledToggles.data();

        wgpu::InstanceDescriptor nativeDesc;
        nativeDesc.nextInChain = &togglesDesc;

        // Build the wire instance descriptor.
        wgpu::DawnWireWGSLControl wgslControl;
        wgslControl.nextInChain = &blocklist;
        wgslControl.enableExperimental = spec.allowUnsafeAPIs || spec.exposeExperimental;
        wgslControl.enableUnsafe = spec.allowUnsafeAPIs;
        wgslControl.enableTesting = spec.useTestingFeatures;

        wgpu::InstanceDescriptor wireDesc;
        wireDesc.nextInChain = &wgslControl;

        ValidationTest::SetUp(&nativeDesc, &wireDesc);
    }
};

wgpu::WGSLLanguageFeatureName kNonExistentFeature = {};

// Check HasFeature for an Instance that doesn't have unsafe APIs.
TEST_F(WGSLFeatureValidationTest, HasFeatureDefaultInstance) {
    SetUp({});

    // Shipped features are present.
    ASSERT_TRUE(
        instance.HasWGSLLanguageFeature(wgpu::WGSLLanguageFeatureName::ChromiumTestingShipped));
    ASSERT_TRUE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingShippedWithKillswitch));

    // Experimental and unimplemented features are not present.
    ASSERT_FALSE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingExperimental));
    ASSERT_FALSE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingUnsafeExperimental));
    ASSERT_FALSE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingUnimplemented));

    // Non-existent features are not present.
    ASSERT_FALSE(instance.HasWGSLLanguageFeature(kNonExistentFeature));
}

// Check HasFeature for an Instance that has unsafe APIs.
TEST_F(WGSLFeatureValidationTest, HasFeatureExposeExperimental) {
    SetUp({.exposeExperimental = true});

    // Shipped and experimental features are present.
    ASSERT_TRUE(
        instance.HasWGSLLanguageFeature(wgpu::WGSLLanguageFeatureName::ChromiumTestingShipped));
    ASSERT_TRUE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingShippedWithKillswitch));
    ASSERT_TRUE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingExperimental));

    // Unsafe and unimplemented features are not present.
    ASSERT_FALSE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingUnsafeExperimental));
    ASSERT_FALSE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingUnimplemented));

    // Non-existent features are not present.
    ASSERT_FALSE(instance.HasWGSLLanguageFeature(kNonExistentFeature));
}

// Check HasFeature for an Instance that has unsafe APIs.
TEST_F(WGSLFeatureValidationTest, HasFeatureAllowUnsafeInstance) {
    SetUp({.allowUnsafeAPIs = true});

    // Shipped and experimental features are present.
    ASSERT_TRUE(
        instance.HasWGSLLanguageFeature(wgpu::WGSLLanguageFeatureName::ChromiumTestingShipped));
    ASSERT_TRUE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingShippedWithKillswitch));
    ASSERT_TRUE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingExperimental));
    ASSERT_TRUE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingUnsafeExperimental));

    // Unimplemented features are not present.
    ASSERT_FALSE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingUnimplemented));

    // Non-existent features are not present.
    ASSERT_FALSE(instance.HasWGSLLanguageFeature(kNonExistentFeature));
}

// Check HasFeature for an Instance that doesn't have the expose_wgsl_testing_features toggle.
TEST_F(WGSLFeatureValidationTest, HasFeatureWithoutExposeWGSLTestingFeatures) {
    SetUp({.useTestingFeatures = false});

    // None of the testing features are present.
    ASSERT_FALSE(
        instance.HasWGSLLanguageFeature(wgpu::WGSLLanguageFeatureName::ChromiumTestingShipped));
    ASSERT_FALSE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingShippedWithKillswitch));
    ASSERT_FALSE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingExperimental));
    ASSERT_FALSE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingUnsafeExperimental));
    ASSERT_FALSE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingUnimplemented));
}

// Tests for the behavior of getting WGSL language features.
TEST_F(WGSLFeatureValidationTest, GetFeatures) {
    SetUp({});

    wgpu::SupportedWGSLLanguageFeatures supportedFeatures = {};
    instance.GetWGSLLanguageFeatures(&supportedFeatures);
    ASSERT_NE(0u, supportedFeatures.featureCount);
    const wgpu::WGSLLanguageFeatureName* features = supportedFeatures.features;

    // Exactly featureCount features should be written, and all return true in HasWGSLFeature.
    for (size_t i = 0; i < supportedFeatures.featureCount; i++) {
        DAWN_UNSAFE_TODO(ASSERT_TRUE(instance.HasWGSLLanguageFeature(features[i])));
    }

    // Test the presence / absence of some known testing features.
    const wgpu::WGSLLanguageFeatureName* begin = features;
    const wgpu::WGSLLanguageFeatureName* end =
        DAWN_UNSAFE_TODO(features + supportedFeatures.featureCount);
    ASSERT_NE(std::find(begin, end, wgpu::WGSLLanguageFeatureName::ChromiumTestingShipped), end);
    ASSERT_NE(
        std::find(begin, end, wgpu::WGSLLanguageFeatureName::ChromiumTestingShippedWithKillswitch),
        end);

    ASSERT_EQ(std::find(begin, end, wgpu::WGSLLanguageFeatureName::ChromiumTestingUnimplemented),
              end);
    ASSERT_EQ(
        std::find(begin, end, wgpu::WGSLLanguageFeatureName::ChromiumTestingUnsafeExperimental),
        end);
    ASSERT_EQ(std::find(begin, end, wgpu::WGSLLanguageFeatureName::ChromiumTestingExperimental),
              end);
}

// Check that the enabled / disabled features are used to validate the WGSL shaders.
TEST_F(WGSLFeatureValidationTest, UsingFeatureInShaderModule) {
    SetUp({});

    utils::CreateShaderModule(device, R"(
        requires chromium_testing_shipped;
    )");
    utils::CreateShaderModule(device, R"(
        requires chromium_testing_shipped_with_killswitch;
    )");

    ASSERT_DEVICE_ERROR(utils::CreateShaderModule(device, R"(
        requires chromium_testing_unimplemented;
    )"));
    ASSERT_DEVICE_ERROR(utils::CreateShaderModule(device, R"(
        requires chromium_testing_unsafe_experimental;
    )"));
    ASSERT_DEVICE_ERROR(utils::CreateShaderModule(device, R"(
        requires chromium_testing_experimental;
    )"));
}

// Test using DawnWGSLBlocklist to block features with a killswitch by name.
TEST_F(WGSLFeatureValidationTest, BlockListOfKillswitchedFeatures) {
    SetUp({.allowUnsafeAPIs = true, .blocklist = {"chromium_testing_shipped_with_killswitch"}});

    // The blocklisted feature is not present.
    ASSERT_FALSE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingShippedWithKillswitch));

    // The others are.
    ASSERT_TRUE(
        instance.HasWGSLLanguageFeature(wgpu::WGSLLanguageFeatureName::ChromiumTestingShipped));
    ASSERT_TRUE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingExperimental));
    ASSERT_TRUE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingUnsafeExperimental));

    // Using the blocklisted extension fails.
    ASSERT_DEVICE_ERROR(utils::CreateShaderModule(device, R"(
        requires chromium_testing_shipped_with_killswitch;
    )"));
}

// Test that DawnWGSLBlocklist can block any feature name (even without a killswitch).
TEST_F(WGSLFeatureValidationTest, BlockListOfAnyFeature) {
    SetUp({.allowUnsafeAPIs = true,
           .blocklist = {"chromium_testing_shipped", "chromium_testing_experimental",
                         "chromium_testing_unsafe_experimental"}});

    // All blocklisted features aren't present.
    ASSERT_FALSE(
        instance.HasWGSLLanguageFeature(wgpu::WGSLLanguageFeatureName::ChromiumTestingShipped));
    ASSERT_FALSE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingExperimental));
    ASSERT_FALSE(instance.HasWGSLLanguageFeature(
        wgpu::WGSLLanguageFeatureName::ChromiumTestingUnsafeExperimental));
}

// Test that DawnWGSLBlocklist can contain garbage names without causing problems.
TEST_F(WGSLFeatureValidationTest, BlockListGarbageName) {
    SetUp({.blocklist = {"LE_GARBAGE"}});
    ASSERT_NE(instance, nullptr);
}

}  // anonymous namespace
}  // namespace dawn
