#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using GitHub.Actions.Expressions;
using GitHub.Actions.Expressions.Data;
using GitHub.Actions.Expressions.Sdk;
using GitHub.Actions.Expressions.Sdk.Functions;
using GitHub.Actions.WorkflowParser.ObjectTemplating;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;

namespace GitHub.Actions.WorkflowParser.Conversion
{
    internal static class WorkflowTemplateConverter
    {
        /// <summary>
        /// Constructs the <c ref="WorkflowTemplate" />. Errors are stored to both <c ref="TemplateContext.Errors" /> and <c ref="WorkflowTemplate.Errors" />.
        /// </summary>
        internal static WorkflowTemplate ConvertToWorkflow(
            TemplateContext context,
            TemplateToken workflow)
        {
            var result = new WorkflowTemplate();
            result.FileTable.AddRange(context.GetFileTable());

            // Note, the "finally" block appends context.Errors to result
            try
            {
                if (workflow == null || context.Errors.Count > 0)
                {
                    return result;
                }

                var workflowMapping = workflow.AssertMapping("root");

                foreach (var workflowPair in workflowMapping)
                {
                    var workflowKey = workflowPair.Key.AssertString("root key");

                    switch (workflowKey.Value)
                    {
                        case WorkflowTemplateConstants.On:
                            var inputTypes = ConvertToOnWorkflowDispatchInputTypes(workflowPair.Value);
                            foreach (var item in inputTypes)
                            {
                                result.InputTypes.TryAdd(item.Key, item.Value);
                            }
                            break;

                        case WorkflowTemplateConstants.Description:
                        case WorkflowTemplateConstants.Name:
                        case WorkflowTemplateConstants.RunName:
                            break;

                        case WorkflowTemplateConstants.Defaults:
                            result.Defaults = workflowPair.Value;
                            break;

                        case WorkflowTemplateConstants.Env:
                            result.Env = workflowPair.Value;
                            break;

                        case WorkflowTemplateConstants.Concurrency:
                            ConvertToConcurrency(context, workflowPair.Value, isEarlyValidation: true);
                            result.Concurrency = workflowPair.Value;
                            break;

                        case WorkflowTemplateConstants.Jobs:
                            result.Jobs.AddRange(ConvertToJobs(context, workflowPair.Value));
                            break;

                        case WorkflowTemplateConstants.Permissions:
                            result.Permissions = ConvertToPermissions(context, workflowPair.Value);
                            break;

                        default:
                            workflowKey.AssertUnexpectedValue("root key"); // throws
                            break;
                    }
                }

                // Propagate explicit permissions
                if (result.Permissions != null)
                {
                    foreach (var job in result.Jobs)
                    {
                        if (job.Permissions == null)
                        {
                            job.Permissions = result.Permissions;
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                context.Errors.Add(ex);
            }
            finally
            {
                if (context.Errors.Count > 0)
                {
                    foreach (var error in context.Errors)
                    {
                        result.Errors.Add(new WorkflowValidationError(error.Code, error.Message));
                    }
                }
            }

            return result;
        }

        internal static void ConvertToReferencedWorkflow(
            TemplateContext context,
            TemplateToken referencedWorkflow,
            ReusableWorkflowJob workflowJob,
            String permissionsPolicy,
            bool isTrusted)
        {
            // Explicit max permissions for the reusable workflow or reusable workflow chain.
            // Present only when the caller (or higher ancestor) has defined the maximum allowed permissions in YAML.
            var explicitMaxPermissions = workflowJob.Permissions;

            var workflowMapping = referencedWorkflow.AssertMapping("root");
            foreach (var workflowPair in workflowMapping)
            {
                var workflowKey = workflowPair.Key.AssertString("root key");
                switch (workflowKey.Value)
                {
                    case WorkflowTemplateConstants.On:
                        ConvertToOnTrigger(context, workflowPair.Value, workflowJob);
                        break;

                    case WorkflowTemplateConstants.Description:
                    case WorkflowTemplateConstants.Name:
                    case WorkflowTemplateConstants.RunName:
                        break;

                    case WorkflowTemplateConstants.Defaults:
                        workflowJob.Defaults = workflowPair.Value;
                        break;

                    case WorkflowTemplateConstants.Env:
                        workflowJob.Env = workflowPair.Value;
                        break;

                    case WorkflowTemplateConstants.Concurrency:
                        ConvertToConcurrency(context, workflowPair.Value, true);
                        workflowJob.EmbeddedConcurrency = workflowPair.Value;
                        break;
                    case WorkflowTemplateConstants.Jobs:
                        workflowJob.Jobs.AddRange(ConvertToJobs(context, workflowPair.Value));
                        break;

                    case WorkflowTemplateConstants.Permissions:
                        var embeddedRootPermissions = ConvertToPermissions(context, workflowPair.Value);
                        PermissionsHelper.ValidateEmbeddedPermissions(
                            context,
                            workflowJob,
                            embeddedJob: null,
                            requested: embeddedRootPermissions,
                            explicitMax: explicitMaxPermissions,
                            permissionsPolicy,
                            isTrusted);
                        workflowJob.Permissions = embeddedRootPermissions;
                        break;

                    default:
                        workflowKey.AssertUnexpectedValue("root key"); // throws
                        break;
                }
            }

            // Validate requested permissions or propagate explicit permissions
            foreach (var embeddedJob in workflowJob.Jobs)
            {
                if (embeddedJob.Permissions != null)
                {
                    // Validate requested permissions
                    PermissionsHelper.ValidateEmbeddedPermissions(
                        context,
                        workflowJob,
                        embeddedJob,
                        requested: embeddedJob.Permissions,
                        explicitMax: explicitMaxPermissions,
                        permissionsPolicy,
                        isTrusted);
                }
                else if (workflowJob.Permissions != null)
                {
                    // Propagate explicit permissions
                    embeddedJob.Permissions = workflowJob.Permissions;
                }
            }
        }

        internal static IDictionary<String, String> ConvertToOnWorkflowDispatchInputTypes(TemplateToken onToken)
        {
            var result = new Dictionary<String, String>();
            if (onToken.Type != TokenType.Mapping)
            {
                return result;
            }

            var triggerMapping = onToken.AssertMapping($"workflow {WorkflowTemplateConstants.On} value");
            var dispatchTrigger = triggerMapping.FirstOrDefault(x =>
                string.Equals((x.Key as StringToken).Value, WorkflowTemplateConstants.WorkflowDispatch, StringComparison.Ordinal)).Value;
            if (dispatchTrigger == null || dispatchTrigger is NullToken)
            {
                return result;
            }

            var wfDispatchDefinitions = dispatchTrigger.AssertMapping($"workflow {WorkflowTemplateConstants.On} value {WorkflowTemplateConstants.WorkflowDispatch}");
            var inputDefinitionsToken = wfDispatchDefinitions.FirstOrDefault(x =>
                string.Equals((x.Key as StringToken).Value, WorkflowTemplateConstants.Inputs, StringComparison.Ordinal)).Value;
            if (inputDefinitionsToken == null || inputDefinitionsToken is NullToken)
            {
                return result;
            }

            var inputs = inputDefinitionsToken.AssertMapping($"{WorkflowTemplateConstants.On}-{WorkflowTemplateConstants.WorkflowDispatch}-{WorkflowTemplateConstants.Inputs}");
            var inputDefinitions = inputs?
                .ToDictionary(
                    x => x.Key.AssertString("inputs key").Value,
                    x => x.Value,
                    StringComparer.OrdinalIgnoreCase
                );
            foreach (var definedItem in inputDefinitions)
            {
                string definedKey = definedItem.Key;

                if (definedItem.Value is NullToken)
                {
                    result.Add(definedKey, WorkflowTemplateConstants.TypeString);
                    continue;
                }

                var definedInputSpec = definedItem.Value.AssertMapping($"input {definedKey}").ToDictionary(
                        x => x.Key.AssertString($"input {definedKey} key").Value,
                        x => x.Value,
                        StringComparer.OrdinalIgnoreCase);
                if (definedInputSpec.TryGetValue(WorkflowTemplateConstants.Type, out TemplateToken inputTypeToken) &&
                    inputTypeToken is StringToken inputTypeStringToken)
                {
                    result.Add(definedKey, inputTypeStringToken.Value);
                }
                else
                {
                    result.Add(definedKey, WorkflowTemplateConstants.TypeString);
                }
            }
            return result;
        }

        internal static void ConvertToOnTrigger(
            TemplateContext context,
            TemplateToken onToken,
            ReusableWorkflowJob parentWorkflowJob)
        {
            switch (onToken.Type)
            {
                // check for on: workflow_call
                case TokenType.String:
                    var result = onToken.AssertString($"Reference workflow {WorkflowTemplateConstants.On} value");
                    if (result.Value == WorkflowTemplateConstants.WorkflowCall)
                    {
                        ValidateWorkflowJobTrigger(context, parentWorkflowJob);
                        return;
                    }
                    break;

                // check for on: [push, workflow_call]
                case TokenType.Sequence:
                    var triggers = onToken.AssertSequence($"Reference workflow {WorkflowTemplateConstants.On} value");

                    foreach (var triggerItem in triggers)
                    {
                        var triggerString = triggerItem.AssertString($"Reference workflow {WorkflowTemplateConstants.On} value {triggerItem}").Value;
                        if (triggerString == WorkflowTemplateConstants.WorkflowCall)
                        {
                            ValidateWorkflowJobTrigger(context, parentWorkflowJob);
                            return;
                        }
                    }
                    break;

                // check for on: workflow_call Mapping
                case TokenType.Mapping:
                    var triggerMapping = onToken.AssertMapping($"Reference workflow {WorkflowTemplateConstants.On} value");

                    foreach (var triggerItem in triggerMapping)
                    {
                        var triggerString = triggerItem.Key.AssertString($"Reference workflow {WorkflowTemplateConstants.On} value {triggerItem.Key}").Value;
                        if (triggerString == WorkflowTemplateConstants.WorkflowCall)
                        {
                            if (triggerItem.Value is NullToken)
                            {
                                ValidateWorkflowJobTrigger(context, parentWorkflowJob);
                                return;
                            }

                            var wfCallDefinitions = triggerItem.Value.AssertMapping($"Reference workflow {WorkflowTemplateConstants.On} value {triggerItem.Key}");

                            foreach (var wfCallDefinitionItem in wfCallDefinitions)
                            {
                                var wfCallDefinitionItemKey = wfCallDefinitionItem.Key.AssertString($"{WorkflowTemplateConstants.On}-{WorkflowTemplateConstants.WorkflowCall}-{wfCallDefinitionItem.Key.ToString()}").Value;
                                if (wfCallDefinitionItemKey == WorkflowTemplateConstants.Inputs)
                                {
                                    parentWorkflowJob.InputDefinitions = wfCallDefinitionItem.Value.AssertMapping($"{WorkflowTemplateConstants.On}-{WorkflowTemplateConstants.WorkflowCall}-{wfCallDefinitionItem.Key.ToString()}");
                                }
                                else if (wfCallDefinitionItemKey == WorkflowTemplateConstants.Secrets)
                                {
                                    parentWorkflowJob.SecretDefinitions = wfCallDefinitionItem.Value.AssertMapping($"{WorkflowTemplateConstants.On}-{WorkflowTemplateConstants.WorkflowCall}-{wfCallDefinitionItem.Key.ToString()}");
                                }
                                else if (wfCallDefinitionItemKey == WorkflowTemplateConstants.Outputs)
                                {
                                    parentWorkflowJob.Outputs = wfCallDefinitionItem.Value.AssertMapping($"{WorkflowTemplateConstants.On}-{WorkflowTemplateConstants.WorkflowCall}-{wfCallDefinitionItem.Key.ToString()}");
                                }
                            }

                            ValidateWorkflowJobTrigger(context, parentWorkflowJob);
                            return;
                        }
                    }
                    break;
                default:
                    break;
            }

            context.Error(onToken, $"{WorkflowTemplateConstants.WorkflowCall} key is not defined in the referenced workflow.");
            return;
        }

        internal static Boolean ConvertToIfResult(
            TemplateContext context,
            TemplateToken ifResult)
        {
            var expression = ifResult.Traverse().FirstOrDefault(x => x is ExpressionToken);
            if (expression != null)
            {
                throw new ArgumentException($"Unexpected type '{expression.GetType().Name}' encountered while reading 'if'.");
            }

            var evaluationResult = EvaluationResult.CreateIntermediateResult(null, ifResult);
            return evaluationResult.IsTruthy;
        }

        internal static String ConvertToJobName(
            TemplateContext context,
            TemplateToken name,
            Boolean isEarlyValidation = false)
        {
            var result = default(String);

            // Expression
            if (isEarlyValidation && name is ExpressionToken)
            {
                return result;
            }

            // String
            var nameString = name.AssertString($"job {WorkflowTemplateConstants.Name}");
            result = nameString.Value;
            return result;
        }

        internal static Snapshot ConvertToSnapshot(
            TemplateContext context,
            TemplateToken snapshotToken,
            Boolean isEarlyValidation = false)
        {
            String imageName = null;
            string defaultVersion = "1.*";
            String version = null;
            var condition = new BasicExpressionToken(null, null, null, $"{WorkflowTemplateConstants.Success}()");

            if (isEarlyValidation && snapshotToken is ExpressionToken)
            {
                return default;
            }


            // String
            if (snapshotToken is StringToken snapshotStringToken)
            {
                imageName = snapshotStringToken.Value;
            }
            // Mapping
            else if (snapshotToken is MappingToken snapshotMappingToken)
            {
                foreach (var snapshotProperty in snapshotMappingToken)
                {
                    var propertyName = snapshotProperty.Key.AssertString($"job {WorkflowTemplateConstants.Snapshot} key");
                    var propertyValue = snapshotProperty.Value;

                    switch (propertyName.Value)
                    {
                        case WorkflowTemplateConstants.ImageName:
                            if (isEarlyValidation && propertyValue is ExpressionToken)
                            {
                                return default;
                            }
                            imageName = propertyValue.AssertString($"job {WorkflowTemplateConstants.Snapshot} {WorkflowTemplateConstants.ImageName}").Value;
                            break;
                        case WorkflowTemplateConstants.If:
                            condition = ConvertToIfCondition(context, propertyValue, IfKind.Snapshot);
                            break;
                        case WorkflowTemplateConstants.CustomImageVersion:
                            if (isEarlyValidation && propertyValue is ExpressionToken)
                            {
                                return default;
                            }
                            var versionValue = propertyValue.AssertString($"job {WorkflowTemplateConstants.Snapshot} {WorkflowTemplateConstants.CustomImageVersion}").Value;
                            if (versionValue != null && !IsSnapshotImageVersionValid(versionValue))
                            {
                                context.Error(snapshotToken, "Expected format '{major-version}.*' Actual '" + versionValue + "'");
                                return null;
                            }
                            version = versionValue;
                            break;
                        default:
                            propertyName.AssertUnexpectedValue($"job {WorkflowTemplateConstants.Snapshot} key");
                            break;
                    }
                }
            }

            // ImageName is a required property (schema validation)
            if (imageName == null)
            {
                context.Error(snapshotToken, $"job {WorkflowTemplateConstants.Snapshot} {WorkflowTemplateConstants.ImageName} is required.");
                return null;
            }

            return new Snapshot
            {
                ImageName = imageName,
                If = condition,
                Version = version ?? defaultVersion
            };
        }

        private static bool IsSnapshotImageVersionValid(string versionString)
        {
            var versionSegments = versionString.Split(".");

            if (versionSegments.Length != 2 ||
                !versionSegments[1].Equals("*") ||
                !Int32.TryParse(versionSegments[0], NumberStyles.None, CultureInfo.InvariantCulture, result: out int parsedMajor) ||
                parsedMajor < 0)
            {
                return false;
            }

            return true;
        }

        internal static RunsOn ConvertToRunsOn(
            TemplateContext context,
            TemplateToken runsOn,
            Boolean isEarlyValidation = false)
        {
            var result = new RunsOn();

            ConvertToRunsOnLabels(context, runsOn, result, isEarlyValidation);

            // Mapping
            if (runsOn is MappingToken runsOnMapping)
            {
                foreach (var runsOnToken in runsOnMapping)
                {
                    var propertyName = runsOnToken.Key.AssertString($"job {WorkflowTemplateConstants.RunsOn} property name");

                    switch (propertyName.Value)
                    {
                        case WorkflowTemplateConstants.Group:
                            // Expression
                            if (isEarlyValidation && runsOnToken.Value is ExpressionToken)
                            {
                                continue;
                            }

                            // String
                            var groupName = runsOnToken.Value.AssertString($"job {WorkflowTemplateConstants.RunsOn} {WorkflowTemplateConstants.Group} name").Value;
                            var names = groupName.Split(WorkflowTemplateConstants.Slash);
                            if (names.Length == 2)
                            {
                                if (string.IsNullOrEmpty(names[1]))
                                {
                                    context.Error(runsOnToken.Value, $"Invalid {WorkflowTemplateConstants.RunsOn} {WorkflowTemplateConstants.Group} name '{groupName}'.");
                                }
                                else if (!string.Equals(names[0], WorkflowTemplateConstants.Org) && !string.Equals(names[0], WorkflowTemplateConstants.Organization) &&
                                    !string.Equals(names[0], WorkflowTemplateConstants.Ent) && !string.Equals(names[0], WorkflowTemplateConstants.Enterprise))
                                {

                                    context.Error(runsOnToken.Value, $"Invalid {WorkflowTemplateConstants.RunsOn} {WorkflowTemplateConstants.Group} name '{groupName}'. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.");
                                }
                                else
                                {
                                    result.RunnerGroup = groupName;
                                }
                            }
                            else if (names.Length > 2)
                            {
                                context.Error(runsOnToken.Value, $"Invalid {WorkflowTemplateConstants.RunsOn} {WorkflowTemplateConstants.Group} name '{groupName}'. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.");
                            }
                            else
                            {
                                result.RunnerGroup = groupName;
                            }
                            break;
                        case WorkflowTemplateConstants.Labels:
                            ConvertToRunsOnLabels(context, runsOnToken.Value, result, isEarlyValidation);
                            break;
                    }
                }
            }

            return result;
        }

        internal static void ConvertToRunsOnLabels(
            TemplateContext context,
            TemplateToken runsOnLabelsToken,
            RunsOn runsOn,
            Boolean isEarlyValidation = false)
        {
            // Expression
            if (isEarlyValidation && runsOnLabelsToken is ExpressionToken)
            {
                return;
            }

            // String
            if (runsOnLabelsToken is StringToken runsOnLabelsString)
            {
                runsOn.Labels.Add(runsOnLabelsString.Value);
            }
            // Sequence<String>
            else if (runsOnLabelsToken is SequenceToken runsOnLabelsSequence)
            {
                foreach (var runsOnLabelToken in runsOnLabelsSequence)
                {
                    // Expression
                    if (isEarlyValidation && runsOnLabelToken is ExpressionToken)
                    {
                        continue;
                    }

                    // String
                    var label = runsOnLabelToken.AssertString($"job {WorkflowTemplateConstants.RunsOn} {WorkflowTemplateConstants.Labels} sequence item");
                    runsOn.Labels.Add(label.Value);
                }
            }
        }

        internal static Int32? ConvertToJobTimeout(
            TemplateContext context,
            TemplateToken token,
            Boolean isEarlyValidation = false)
        {
            if (isEarlyValidation && token is ExpressionToken)
            {
                return null;
            }

            var numberToken = token.AssertNumber($"job {WorkflowTemplateConstants.TimeoutMinutes}");
            return (Int32)numberToken.Value;
        }

        internal static Int32? ConvertToJobCancelTimeout(
            TemplateContext context,
            TemplateToken token,
            Boolean isEarlyValidation = false)
        {
            if (isEarlyValidation && token is ExpressionToken)
            {
                return null;
            }

            var numberToken = token.AssertNumber($"job {WorkflowTemplateConstants.CancelTimeoutMinutes}");
            return (Int32)numberToken.Value;
        }

        internal static Boolean? ConvertToJobContinueOnError(
            TemplateContext context,
            TemplateToken token,
            Boolean isEarlyValidation = false)
        {
            if (isEarlyValidation && token is ExpressionToken)
            {
                return null;
            }

            var booleanToken = token.AssertBoolean($"job {WorkflowTemplateConstants.ContinueOnError}");
            return booleanToken.Value;
        }

        internal static Boolean? ConvertToStepContinueOnError(
            TemplateContext context,
            TemplateToken token,
            Boolean isEarlyValidation = false)
        {
            if (isEarlyValidation && token is ExpressionToken)
            {
                return null;
            }

            var booleanToken = token.AssertBoolean($"step {WorkflowTemplateConstants.ContinueOnError}");
            return booleanToken.Value;
        }

        internal static String ConvertToStepName(
            TemplateContext context,
            TemplateToken token,
            Boolean isEarlyValidation = false)
        {
            if (isEarlyValidation && token is ExpressionToken)
            {
                return null;
            }

            var stringToken = token.AssertString($"step {WorkflowTemplateConstants.Name}");
            return stringToken.Value;
        }

        internal static GroupPermitSetting ConvertToConcurrency(
            TemplateContext context,
            TemplateToken concurrency,
            Boolean isEarlyValidation = false)
        {
            var result = new GroupPermitSetting("");

            // Expression
            if (isEarlyValidation && concurrency is ExpressionToken)
            {
                return result;
            }

            // String
            if (concurrency is StringToken concurrencyString)
            {
                result.Group = concurrencyString.Value;
            }
            // Mapping
            else
            {
                var concurrencyMapping = concurrency.AssertMapping($"{WorkflowTemplateConstants.Concurrency}");
                foreach (var concurrencyProperty in concurrencyMapping)
                {
                    var propertyName = concurrencyProperty.Key.AssertString($"{WorkflowTemplateConstants.Concurrency} key");

                    // Expression
                    if (isEarlyValidation && (concurrencyProperty.Value is ExpressionToken || concurrencyProperty.Key is ExpressionToken))
                    {
                        continue;
                    }

                    switch (propertyName.Value)
                    {
                        case WorkflowTemplateConstants.Group:
                            // Literal
                            var group = concurrencyProperty.Value.AssertString($"{WorkflowTemplateConstants.Group} key");
                            result.Group = group.Value;
                            break;
                        case WorkflowTemplateConstants.CancelInProgress:
                            // Literal
                            var cancelInProgress = concurrencyProperty.Value.AssertBoolean($"{WorkflowTemplateConstants.CancelInProgress} key");
                            result.CancelInProgress = cancelInProgress.Value;
                            break;
                        default:
                            propertyName.AssertUnexpectedValue($"{WorkflowTemplateConstants.Concurrency} key"); // throws
                            break;
                    }
                }
            }

            if (!isEarlyValidation && String.IsNullOrEmpty(result.Group))
            {
                context.Error(concurrency, "Concurrency group name cannot be empty");
            }

            if (result.Group?.Length > 400)
            {
                context.Error(concurrency, "Concurrency group name must be less than 400 characters");
            }

            return result;
        }

        internal static ActionsEnvironmentReference ConvertToActionEnvironmentReference(
            TemplateContext context,
            TemplateToken environment,
            bool isEarlyValidation = false)
        {
            // Expression
            if (isEarlyValidation && environment is ExpressionToken)
            {
                return null;
            }
            // String
            else if (environment is StringToken nameString)
            {
                return String.IsNullOrEmpty(nameString.Value) ? null : new ActionsEnvironmentReference(nameString.Value);
            }
            // Mapping
            else
            {
                var environmentMapping = environment.AssertMapping($"job {WorkflowTemplateConstants.Environment}");

                if (isEarlyValidation)
                {
                    // Skip early validation if any expressions other than "url" (expanded by the runner)
                    var urlToken = environmentMapping
                        .Where(x => x.Key is StringToken key && string.Equals(key.Value, WorkflowTemplateConstants.Url, StringComparison.Ordinal))
                        .Select(x => x.Value)
                        .SingleOrDefault();
                    if (isEarlyValidation && environmentMapping.Traverse().Any(x => x is ExpressionToken && x != urlToken))
                    {
                        return null;
                    }
                }

                var name = default(String);
                var url = default(TemplateToken);
                foreach (var environmentProp in environmentMapping)
                {
                    var propertyName = environmentProp.Key.AssertString($"job {WorkflowTemplateConstants.Environment} key");
                    var propertyValue = environmentProp.Value;

                    switch (propertyName.Value)
                    {
                        // Name is a required property (schema validation)
                        case WorkflowTemplateConstants.Name:
                            name = propertyValue.AssertString($"job {WorkflowTemplateConstants.Environment} {WorkflowTemplateConstants.Name}").Value;
                            break;
                        case WorkflowTemplateConstants.Url:
                            url = propertyValue;
                            break;
                        default:
                            propertyName.AssertUnexpectedValue($"job {WorkflowTemplateConstants.Environment} key"); // throws
                            break;
                    }
                }

                if (!String.IsNullOrEmpty(name))
                {
                    return new ActionsEnvironmentReference(name) { Url = url };
                }
                else
                {
                    return null;
                }
            }
        }

        internal static Dictionary<String, String> ConvertToStepEnvironment(
            TemplateContext context,
            TemplateToken environment,
            StringComparer keyComparer,
            Boolean isEarlyValidation = false)
        {
            var result = new Dictionary<String, String>(keyComparer);

            // Expression
            if (isEarlyValidation && environment is ExpressionToken)
            {
                return result;
            }

            // Mapping
            var mapping = environment.AssertMapping("environment");

            foreach (var pair in mapping)
            {
                // Expression key
                if (isEarlyValidation && pair.Key is ExpressionToken)
                {
                    continue;
                }

                // String key
                var key = pair.Key.AssertString("environment key");

                // Expression value
                if (isEarlyValidation && pair.Value is ExpressionToken)
                {
                    continue;
                }

                // String value
                var value = pair.Value.AssertString("environment value");
                result[key.Value] = value.Value;
            }

            return result;
        }

        internal static Dictionary<String, String> ConvertToStepInputs(
            TemplateContext context,
            TemplateToken inputs,
            Boolean isEarlyValidation = false)
        {
            var result = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);

            // Expression
            if (isEarlyValidation && inputs is ExpressionToken)
            {
                return result;
            }

            // Mapping
            var mapping = inputs.AssertMapping("inputs");

            foreach (var pair in mapping)
            {
                // Expression key
                if (isEarlyValidation && pair.Key is ExpressionToken)
                {
                    continue;
                }

                // Literal key
                var key = pair.Key.AssertString("inputs key");

                // Expression value
                if (isEarlyValidation && pair.Value is ExpressionToken)
                {
                    continue;
                }

                // Literal value
                var value = pair.Value.AssertString("inputs value");
                result[key.Value] = value.Value;
            }

            return result;
        }

        internal static Int32? ConvertToStepTimeout(
            TemplateContext context,
            TemplateToken token,
            Boolean isEarlyValidation = false)
        {
            if (isEarlyValidation && token is ExpressionToken)
            {
                return null;
            }

            var numberToken = token.AssertNumber($"step {WorkflowTemplateConstants.TimeoutMinutes}");
            return (Int32)numberToken.Value;
        }

        internal static Strategy ConvertToStrategy(
            TemplateContext context,
            TemplateToken token,
            String jobFactoryName,
            Boolean isEarlyValidation = false)
        {
            var result = new Strategy();

            // Expression
            if (isEarlyValidation && token is ExpressionToken)
            {
                return result;
            }

            var strategyMapping = token.AssertMapping(WorkflowTemplateConstants.Strategy);
            var matrixBuilder = default(MatrixBuilder);
            var hasExpressions = false;

            foreach (var strategyPair in strategyMapping)
            {
                // Expression key
                if (isEarlyValidation && strategyPair.Key is ExpressionToken)
                {
                    hasExpressions = true;
                    continue;
                }

                // Literal key
                var strategyKey = strategyPair.Key.AssertString("strategy key");

                switch (strategyKey.Value)
                {
                    // Fail-Fast
                    case WorkflowTemplateConstants.FailFast:
                        if (isEarlyValidation && strategyPair.Value is ExpressionToken)
                        {
                            hasExpressions = true;
                            continue;
                        }

                        var failFastBooleanToken = strategyPair.Value.AssertBoolean($"strategy {WorkflowTemplateConstants.FailFast}");
                        result.FailFast = failFastBooleanToken.Value;
                        break;

                    // Max-Parallel
                    case WorkflowTemplateConstants.MaxParallel:
                        if (isEarlyValidation && strategyPair.Value is ExpressionToken)
                        {
                            hasExpressions = true;
                            continue;
                        }

                        var maxParallelNumberToken = strategyPair.Value.AssertNumber($"strategy {WorkflowTemplateConstants.MaxParallel}");
                        result.MaxParallel = (Int32)maxParallelNumberToken.Value;
                        break;

                    // Matrix
                    case WorkflowTemplateConstants.Matrix:

                        // Expression
                        if (isEarlyValidation && strategyPair.Value is ExpressionToken)
                        {
                            hasExpressions = true;
                            continue;
                        }

                        var matrix = strategyPair.Value.AssertMapping("matrix");
                        hasExpressions = hasExpressions || matrix.Traverse().Any(x => x is ExpressionToken);
                        matrixBuilder = new MatrixBuilder(context, jobFactoryName);
                        var hasCrossProductVector = false;
                        var hasIncludeVector = false;

                        foreach (var matrixPair in matrix)
                        {
                            // Expression key
                            if (isEarlyValidation && matrixPair.Key is ExpressionToken)
                            {
                                hasCrossProductVector = true; // For early validation, treat as if a vector is defined
                                continue;
                            }

                            var matrixKey = matrixPair.Key.AssertString("matrix key");
                            switch (matrixKey.Value)
                            {
                                case WorkflowTemplateConstants.Include:
                                    if (isEarlyValidation && matrixPair.Value.Traverse().Any(x => x is ExpressionToken))
                                    {
                                        hasIncludeVector = true; // For early validation, treat as OK
                                        continue;
                                    }

                                    var includeSequence = matrixPair.Value.AssertSequence("matrix includes");
                                    hasIncludeVector = includeSequence.Count > 0;
                                    matrixBuilder.Include(includeSequence);
                                    break;

                                case WorkflowTemplateConstants.Exclude:
                                    if (isEarlyValidation && matrixPair.Value.Traverse().Any(x => x is ExpressionToken))
                                    {
                                        continue;
                                    }

                                    var excludeSequence = matrixPair.Value.AssertSequence("matrix excludes");
                                    matrixBuilder.Exclude(excludeSequence);
                                    break;

                                default:
                                    hasCrossProductVector = true;

                                    if (isEarlyValidation && matrixPair.Value.Traverse().Any(x => x is ExpressionToken))
                                    {
                                        continue;
                                    }

                                    var vectorName = matrixKey.Value;
                                    var vectorSequence = matrixPair.Value.AssertSequence("matrix vector value");
                                    if (vectorSequence.Count == 0)
                                    {
                                        context.Error(vectorSequence, $"Matrix vector '{vectorName}' does not contain any values");
                                    }
                                    else
                                    {
                                        matrixBuilder.AddVector(vectorName, vectorSequence);
                                    }
                                    break;
                            }
                        }

                        if (!hasCrossProductVector && !hasIncludeVector)
                        {
                            context.Error(matrix, $"Matrix must define at least one vector");
                        }

                        break;

                    default:
                        strategyKey.AssertUnexpectedValue("strategy key"); // throws
                        break;
                }
            }

            if (hasExpressions)
            {
                return result;
            }

            if (matrixBuilder != null)
            {
                result.Configurations.AddRange(matrixBuilder.Build());
            }

            for (var i = 0; i < result.Configurations.Count; i++)
            {
                var configuration = result.Configurations[i];

                var strategy = new DictionaryExpressionData()
                {
                    {
                        "fail-fast",
                        new BooleanExpressionData(result.FailFast)
                    },
                    {
                        "job-index",
                        new NumberExpressionData(i)
                    },
                    {
                        "job-total",
                        new NumberExpressionData(result.Configurations.Count)
                    }
                };

                if (result.MaxParallel > 0)
                {
                    strategy.Add(
                        "max-parallel",
                        new NumberExpressionData(result.MaxParallel)
                    );
                }
                else
                {
                    strategy.Add(
                        "max-parallel",
                        new NumberExpressionData(result.Configurations.Count)
                    );
                }

                configuration.ExpressionData.Add(WorkflowTemplateConstants.Strategy, strategy);
                context.Memory.AddBytes(WorkflowTemplateConstants.Strategy);
                context.Memory.AddBytes(strategy, traverse: true);

                if (!configuration.ExpressionData.ContainsKey(WorkflowTemplateConstants.Matrix))
                {
                    configuration.ExpressionData.Add(WorkflowTemplateConstants.Matrix, null);
                    context.Memory.AddBytes(WorkflowTemplateConstants.Matrix);
                }
            }

            return result;
        }

        internal static ContainerRegistryCredentials ConvertToContainerCredentials(TemplateToken token)
        {
            var credentials = token.AssertMapping(WorkflowTemplateConstants.Credentials);
            var result = new ContainerRegistryCredentials();
            foreach (var credentialProperty in credentials)
            {
                var propertyName = credentialProperty.Key.AssertString($"{WorkflowTemplateConstants.Credentials} key");
                switch (propertyName.Value)
                {
                    case WorkflowTemplateConstants.Username:
                        result.Username = credentialProperty.Value.AssertString(WorkflowTemplateConstants.Username).Value;
                        break;
                    case WorkflowTemplateConstants.Password:
                        result.Password = credentialProperty.Value.AssertString(WorkflowTemplateConstants.Password).Value;
                        break;
                    default:
                        propertyName.AssertUnexpectedValue($"{WorkflowTemplateConstants.Credentials} key {propertyName}");
                        break;
                }
            }

            return result;
        }

        internal static JobContainer ConvertToJobContainer(
            TemplateContext context,
            TemplateToken value,
            bool isEarlyValidation = false,
            bool isServiceContainer = false)
        {
            var result = new JobContainer();
            if (isEarlyValidation && value.Traverse().Any(x => x is ExpressionToken))
            {
                return result;
            }

            if (value is StringToken containerLiteral)
            {
                // Trim "docker://"
                var trimmedImage = containerLiteral.Value;
                var hasDockerPrefix = containerLiteral.Value.StartsWith(WorkflowTemplateConstants.DockerUriPrefix, StringComparison.Ordinal);
                if (hasDockerPrefix)
                {
                    trimmedImage = trimmedImage.Substring(WorkflowTemplateConstants.DockerUriPrefix.Length);
                }

                // Empty shorthand after trimming "docker://" ?
                if (String.IsNullOrEmpty(trimmedImage))
                {
                    // Error at parse-time for:
                    //   1. container: 'docker://'
                    //   2. services.foo: ''
                    //   3. services.foo: 'docker://'
                    //
                    // Do not error for:
                    //   1. container: ''
                    if (isEarlyValidation && (hasDockerPrefix || isServiceContainer))
                    {
                        context.Error(value, "Container image cannot be empty");
                    }

                    // Short-circuit
                    return null;
                }

                // Store original, trimmed further below
                result.Image = containerLiteral.Value;
            }
            else
            {
                var containerMapping = value.AssertMapping($"{WorkflowTemplateConstants.Container}");
                foreach (var containerPropertyPair in containerMapping)
                {
                    var propertyName = containerPropertyPair.Key.AssertString($"{WorkflowTemplateConstants.Container} key");

                    switch (propertyName.Value)
                    {
                        case WorkflowTemplateConstants.Image:
                            result.Image = containerPropertyPair.Value.AssertString($"{WorkflowTemplateConstants.Container} {propertyName}").Value;
                            break;
                        case WorkflowTemplateConstants.Env:
                            var env = containerPropertyPair.Value.AssertMapping($"{WorkflowTemplateConstants.Container} {propertyName}");
                            var envDict = new Dictionary<String, String>(env.Count);
                            foreach (var envPair in env)
                            {
                                var envKey = envPair.Key.ToString();
                                var envValue = envPair.Value.AssertString($"{WorkflowTemplateConstants.Container} {propertyName} {envPair.Key.ToString()}").Value;
                                envDict.Add(envKey, envValue);
                            }
                            result.Environment = envDict;
                            break;
                        case WorkflowTemplateConstants.Options:
                            result.Options = containerPropertyPair.Value.AssertString($"{WorkflowTemplateConstants.Container} {propertyName}").Value;
                            break;
                        case WorkflowTemplateConstants.Entrypoint:
                            if (!context.GetFeatures().AllowServiceContainerCommand)
                            {
                                context.Error(containerPropertyPair.Key, $"The key '{WorkflowTemplateConstants.Entrypoint}' is not allowed");
                                break;
                            }
                            result.Entrypoint = containerPropertyPair.Value.AssertString($"{WorkflowTemplateConstants.Container} {propertyName}").Value;
                            break;
                        case WorkflowTemplateConstants.Command:
                            if (!context.GetFeatures().AllowServiceContainerCommand)
                            {
                                context.Error(containerPropertyPair.Key, $"The key '{WorkflowTemplateConstants.Command}' is not allowed");
                                break;
                            }
                            result.Command = containerPropertyPair.Value.AssertString($"{WorkflowTemplateConstants.Container} {propertyName}").Value;
                            break;
                        case WorkflowTemplateConstants.Ports:
                            var ports = containerPropertyPair.Value.AssertSequence($"{WorkflowTemplateConstants.Container} {propertyName}");
                            var portList = new List<String>(ports.Count);
                            foreach (var portItem in ports)
                            {
                                var portString = portItem.AssertString($"{WorkflowTemplateConstants.Container} {propertyName} {portItem.ToString()}").Value;
                                portList.Add(portString);
                            }
                            result.Ports = portList;
                            break;
                        case WorkflowTemplateConstants.Volumes:
                            var volumes = containerPropertyPair.Value.AssertSequence($"{WorkflowTemplateConstants.Container} {propertyName}");
                            var volumeList = new List<String>(volumes.Count);
                            foreach (var volumeItem in volumes)
                            {
                                var volumeString = volumeItem.AssertString($"{WorkflowTemplateConstants.Container} {propertyName} {volumeItem.ToString()}").Value;
                                volumeList.Add(volumeString);
                            }
                            result.Volumes = volumeList;
                            break;
                        case WorkflowTemplateConstants.Credentials:
                            result.Credentials = ConvertToContainerCredentials(containerPropertyPair.Value);
                            break;
                        default:
                            propertyName.AssertUnexpectedValue($"{WorkflowTemplateConstants.Container} key");
                            break;
                    }
                }
            }

            // Trim "docker://"
            var hadDockerPrefix = false;
            if (!String.IsNullOrEmpty(result.Image) && result.Image.StartsWith(WorkflowTemplateConstants.DockerUriPrefix, StringComparison.Ordinal))
            {
                hadDockerPrefix = true;
                result.Image = result.Image.Substring(WorkflowTemplateConstants.DockerUriPrefix.Length);
            }

            if (String.IsNullOrEmpty(result.Image))
            {
                // Error at parse-time for:
                //   1. container: {image: 'docker://'}
                //   2. services.foo: {image: ''}
                //   3. services.foo: {image: 'docker://'}
                //
                // Do not error for:
                //   1. container: {image: ''}
                if (isEarlyValidation && (hadDockerPrefix || isServiceContainer))
                {
                    context.Error(value, "Container image cannot be empty");
                }
                return null;
            }

            return result;
        }

        internal static List<KeyValuePair<String, JobContainer>> ConvertToJobServiceContainers(
            TemplateContext context,
            TemplateToken services,
            bool isEarlyValidation = false)
        {
            var result = new List<KeyValuePair<String, JobContainer>>();

            if (isEarlyValidation && services.Traverse().Any(x => x is ExpressionToken))
            {
                return result;
            }

            var servicesMapping = services.AssertMapping("services");

            foreach (var servicePair in servicesMapping)
            {
                var networkAlias = servicePair.Key.AssertString("services key").Value;
                var container = ConvertToJobContainer(context, servicePair.Value, isEarlyValidation, isServiceContainer: true);
                result.Add(new KeyValuePair<String, JobContainer>(networkAlias, container));
            }

            return result;
        }

        private static IList<IJob> ConvertToJobs(
            TemplateContext context,
            TemplateToken workflow)
        {
            var result = new List<IJob>();
            var jobsMapping = workflow.AssertMapping(WorkflowTemplateConstants.Jobs);
            var ready = new Queue<NodeInfo>();
            var allUnsatisfied = new List<NodeInfo>();
            var jobCountValidator = context.GetJobCountValidator();
            foreach (var jobsPair in jobsMapping)
            {
                var jobId = jobsPair.Key.AssertString($"{WorkflowTemplateConstants.Jobs} key");
                jobCountValidator.Increment(jobId);
                var jobDefinition = jobsPair.Value.AssertMapping($"{WorkflowTemplateConstants.Jobs} value");
                var idBuilder = new IdBuilder();
                var job = ConvertToJob(context, jobId, jobDefinition, idBuilder);
                result.Add(job);
                var nodeInfo = new NodeInfo
                {
                    Name = job.Id!.Value,
                    Needs = new List<StringToken>(job.Needs ?? new List<StringToken>()),
                };
                if (nodeInfo.Needs.Count == 0)
                {
                    ready.Enqueue(nodeInfo);
                }
                else
                {
                    allUnsatisfied.Add(nodeInfo);
                }
            }

            if (context.Errors.Count != 0)
            {
                return result;
            }

            if (ready.Count == 0)
            {
                context.Error(jobsMapping, "The workflow must contain at least one job with no dependencies.");
                return result;
            }

            while (ready.Count > 0)
            {
                var current = ready.Dequeue();

                // Figure out which nodes would start after current completes
                for (var i = allUnsatisfied.Count - 1; i >= 0; i--)
                {
                    var unsatisfied = allUnsatisfied[i];
                    for (var j = unsatisfied.Needs.Count - 1; j >= 0; j--)
                    {
                        if (String.Equals(unsatisfied.Needs[j].Value, current.Name, StringComparison.OrdinalIgnoreCase))
                        {
                            unsatisfied.Needs.RemoveAt(j);
                            if (unsatisfied.Needs.Count == 0)
                            {
                                ready.Enqueue(unsatisfied);
                                allUnsatisfied.RemoveAt(i);
                            }
                        }
                    }
                }
            }

            // Check whether some jobs will never execute
            if (allUnsatisfied.Count > 0)
            {
                var names = result.ToHashSet(x => x.Id!.Value, StringComparer.OrdinalIgnoreCase);
                foreach (var unsatisfied in allUnsatisfied)
                {
                    foreach (var need in unsatisfied.Needs)
                    {
                        if (names.Contains(need.Value))
                        {
                            context.Error(need, $"Job '{unsatisfied.Name}' depends on job '{need.Value}' which creates a cycle in the dependency graph.");
                        }
                        else
                        {
                            context.Error(need, $"Job '{unsatisfied.Name}' depends on unknown job '{need.Value}'.");
                        }
                    }
                }
            }

            return result;
        }

        private static IJob ConvertToJob(
            TemplateContext context,
            StringToken jobId,
            MappingToken jobDefinition,
            IdBuilder idBuilder)
        {
            if (!idBuilder.TryAddKnownId(jobId.Value, out var error))
            {
                context.Error(jobId, error);
            }

            var condition = new BasicExpressionToken(null, null, null, $"{WorkflowTemplateConstants.Success}()");
            var continueOnError = default(ScalarToken);
            var env = default(TemplateToken);
            var name = default(ScalarToken);
            var jobTarget = default(TemplateToken);
            var steps = new List<IStep>();
            var strategy = default(TemplateToken);
            var jobContainer = default(TemplateToken);
            var jobServiceContainers = default(TemplateToken);
            var concurrency = default(TemplateToken);
            var actionsEnvironment = default(TemplateToken);
            var defaults = default(TemplateToken);
            var permissions = default(Permissions);
            var outputs = default(TemplateToken);
            var jobTimeout = default(ScalarToken);
            var jobCancelTimeout = default(ScalarToken);
            var snapshot = default(TemplateToken);
            var needs = new List<StringToken>();

            var workflowJobRef = default(StringToken);
            var workflowJobInputs = default(MappingToken);
            var workflowJobSecrets = default(MappingToken);
            var workflowJobSecretsInherited = false;

            foreach (var jobProperty in jobDefinition)
            {
                var propertyName = jobProperty.Key.AssertString($"job property name");

                switch (propertyName.Value)
                {
                    case WorkflowTemplateConstants.ContinueOnError:
                        ConvertToJobContinueOnError(context, jobProperty.Value, isEarlyValidation: true); // Validate early if possible
                        continueOnError = jobProperty.Value.AssertScalar($"job {WorkflowTemplateConstants.ContinueOnError}");
                        break;

                    case WorkflowTemplateConstants.If:
                        condition = ConvertToIfCondition(context, jobProperty.Value, IfKind.Job);
                        break;

                    case WorkflowTemplateConstants.Name:
                        name = jobProperty.Value.AssertScalar($"job {WorkflowTemplateConstants.Name}");
                        ConvertToJobName(context, name, isEarlyValidation: true); // Validate early if possible
                        break;

                    case WorkflowTemplateConstants.Needs:
                        if (jobProperty.Value is StringToken needsLiteral)
                        {
                            needs.Add(needsLiteral);
                        }
                        else
                        {
                            var needsSeq = jobProperty.Value.AssertSequence($"job {WorkflowTemplateConstants.Needs}");
                            foreach (var needsItem in needsSeq)
                            {
                                var need = needsItem.AssertString($"job {WorkflowTemplateConstants.Needs} item");
                                needs.Add(need);
                            }
                        }
                        break;

                    case WorkflowTemplateConstants.RunsOn:
                        ConvertToRunsOn(context, jobProperty.Value, isEarlyValidation: true); // Validate early if possible
                        jobTarget = jobProperty.Value;
                        break;

                    case WorkflowTemplateConstants.Snapshot:
                        if (!context.GetFeatures().Snapshot)
                        {
                            context.Error(jobProperty.Key, $"The key '{WorkflowTemplateConstants.Snapshot}' is not allowed");
                            break;
                        }

                        ConvertToSnapshot(context, jobProperty.Value, isEarlyValidation: true); // Validate early if possible
                        snapshot = jobProperty.Value;
                        break;

                    case WorkflowTemplateConstants.Steps:
                        steps.AddRange(ConvertToSteps(context, jobProperty.Value));
                        break;

                    case WorkflowTemplateConstants.Strategy:
                        ConvertToStrategy(context, jobProperty.Value, null, isEarlyValidation: true); // Validate early if possible
                        strategy = jobProperty.Value;
                        break;

                    case WorkflowTemplateConstants.TimeoutMinutes:
                        ConvertToJobTimeout(context, jobProperty.Value, isEarlyValidation: true); // Validate early if possible
                        jobTimeout = jobProperty.Value as ScalarToken;
                        break;

                    case WorkflowTemplateConstants.CancelTimeoutMinutes:
                        ConvertToJobCancelTimeout(context, jobProperty.Value, isEarlyValidation: true); // Validate early if possible
                        jobCancelTimeout = jobProperty.Value as ScalarToken;
                        break;

                    case WorkflowTemplateConstants.Container:
                        ConvertToJobContainer(context, jobProperty.Value, isEarlyValidation: true); // Validate early if possible
                        jobContainer = jobProperty.Value;
                        break;

                    case WorkflowTemplateConstants.Services:
                        ConvertToJobServiceContainers(context, jobProperty.Value, isEarlyValidation: true); // Validate early if possible
                        jobServiceContainers = jobProperty.Value;
                        break;

                    case WorkflowTemplateConstants.Concurrency:
                        ConvertToConcurrency(context, jobProperty.Value, isEarlyValidation: true); // Validate early if possible
                        concurrency = jobProperty.Value;
                        break;
                    case WorkflowTemplateConstants.Env:
                        env = jobProperty.Value;
                        break;

                    case WorkflowTemplateConstants.Environment:
                        ConvertToActionEnvironmentReference(context, jobProperty.Value, isEarlyValidation: true); // Validate early if possible
                        actionsEnvironment = jobProperty.Value;
                        break;

                    case WorkflowTemplateConstants.Outputs:
                        outputs = jobProperty.Value;
                        break;

                    case WorkflowTemplateConstants.Defaults:
                        defaults = jobProperty.Value;
                        break;

                    case WorkflowTemplateConstants.Permissions:
                        permissions = ConvertToPermissions(context, jobProperty.Value);
                        break;

                    case WorkflowTemplateConstants.Uses:
                        workflowJobRef = jobProperty.Value.AssertString($"job {WorkflowTemplateConstants.Uses} value");
                        break;

                    case WorkflowTemplateConstants.With:
                        workflowJobInputs = jobProperty.Value.AssertMapping($"{WorkflowTemplateConstants.Uses}-{WorkflowTemplateConstants.With} value");
                        break;

                    case WorkflowTemplateConstants.Secrets:
                        // String in case inherit is used
                        if (jobProperty.Value is StringToken sToken
                            && sToken.Value == WorkflowTemplateConstants.Inherit)
                        {
                            workflowJobSecretsInherited = true;
                        }
                        else
                        {
                            workflowJobSecrets = jobProperty.Value.AssertMapping($"{WorkflowTemplateConstants.Uses}-{WorkflowTemplateConstants.Secrets} value");
                        }

                        break;

                    default:
                        propertyName.AssertUnexpectedValue("job key"); // throws
                        break;
                }
            }

            if (workflowJobRef != null)
            {
                var workflowJob = new ReusableWorkflowJob
                {
                    Id = jobId,
                    Name = name,
                    If = condition,
                    Ref = workflowJobRef,
                    InputValues = workflowJobInputs,
                    SecretValues = workflowJobSecrets,
                    InheritSecrets = workflowJobSecretsInherited,
                    Permissions = permissions,
                    Concurrency = concurrency,
                    Strategy = strategy,
                };
                if (workflowJob.Name is null || workflowJob.Name is StringToken nameStr && String.IsNullOrEmpty(nameStr.Value))
                {
                    workflowJob.Name = workflowJob.Id;
                }
                workflowJob.Needs.AddRange(needs);

                return workflowJob;
            }
            else
            {
                var result = new Job
                {
                    Id = jobId,
                    Name = name,
                    ContinueOnError = continueOnError,
                    If = condition,
                    RunsOn = jobTarget,
                    Strategy = strategy,
                    TimeoutMinutes = jobTimeout,
                    CancelTimeoutMinutes = jobCancelTimeout,
                    Container = jobContainer,
                    Services = jobServiceContainers,
                    Concurrency = concurrency,
                    Env = env,
                    Environment = actionsEnvironment,
                    Outputs = outputs,
                    Defaults = defaults,
                    Permissions = permissions,
                    Snapshot = snapshot,
                };
                if (result.Name is null || result.Name is StringToken nameStr && String.IsNullOrEmpty(nameStr.Value))
                {
                    result.Name = result.Id;
                }

                result.Needs.AddRange(needs);
                result.Steps.AddRange(steps);
                return result;
            }
        }

        internal static IDictionary<String, String> ConvertToWorkflowJobSecrets(
            TemplateContext context,
            TemplateToken secrets)
        {
            var result = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
            var mapping = secrets.AssertMapping("workflow job secrets");
            foreach (var pair in mapping)
            {
                var key = pair.Key.AssertString("workflow job secret key").Value;
                var value = pair.Value.AssertString("workflow job secret value").Value;
                if (!String.IsNullOrEmpty(value))
                {
                    result[key] = value;
                }
            }

            return result;
        }

        // Public because used by runner for composite actions
        public static List<IStep> ConvertToSteps(
            TemplateContext context,
            TemplateToken steps)
        {
            var stepsSequence = steps.AssertSequence($"job {WorkflowTemplateConstants.Steps}");

            var idBuilder = new IdBuilder();
            var result = stepsSequence.Select(x => ConvertToStep(context, x, idBuilder)).ToList();

            // Generate default IDs when empty
            foreach (IStep step in result)
            {
                if (!string.IsNullOrEmpty(step.Id))
                {
                    continue;
                }

                var id = default(string);
                if (step is ActionStep action)
                {
                    if (action.Uses!.Value.StartsWith(WorkflowTemplateConstants.DockerUriPrefix, StringComparison.Ordinal))
                    {
                        id = action.Uses!.Value.Substring(WorkflowTemplateConstants.DockerUriPrefix.Length);
                    }
                    else if (action.Uses!.Value.StartsWith("./") || action.Uses!.Value.StartsWith(".\\"))
                    {
                        id = WorkflowConstants.SelfAlias;
                    }
                    else
                    {
                        var usesSegments = action.Uses!.Value.Split('@');
                        var pathSegments = usesSegments[0].Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
                        var gitRef = usesSegments.Length == 2 ? usesSegments[1] : String.Empty;

                        if (usesSegments.Length == 2 &&
                            pathSegments.Length >= 2 &&
                            !string.IsNullOrEmpty(pathSegments[0]) &&
                            !string.IsNullOrEmpty(pathSegments[1]) &&
                            !string.IsNullOrEmpty(gitRef))
                        {
                            id = $"{pathSegments[0]}/{pathSegments[1]}";
                        }
                    }
                }

                if (string.IsNullOrEmpty(id))
                {
                    id = "run";
                }

                idBuilder.AppendSegment($"__{id}");

                // Allow reserved prefix "__" for default IDs
                step.Id = idBuilder.Build(allowReservedPrefix: true);
            }

            return result;
        }

        private static IStep ConvertToStep(
            TemplateContext context,
            TemplateToken stepsItem,
            IdBuilder idBuilder)
        {
            var step = stepsItem.AssertMapping($"{WorkflowTemplateConstants.Steps} item");
            var continueOnError = default(ScalarToken);
            var env = default(TemplateToken);
            var id = default(StringToken);
            var ifCondition = default(BasicExpressionToken);
            var ifToken = default(ScalarToken);
            var name = default(ScalarToken);
            var run = default(ScalarToken);
            var timeoutMinutes = default(ScalarToken);
            var uses = default(StringToken);
            var with = default(TemplateToken);
            var workingDir = default(ScalarToken);
            var shell = default(ScalarToken);

            foreach (var stepProperty in step)
            {
                var propertyName = stepProperty.Key.AssertString($"{WorkflowTemplateConstants.Steps} item key");

                switch (propertyName.Value)
                {
                    case WorkflowTemplateConstants.ContinueOnError:
                        ConvertToStepContinueOnError(context, stepProperty.Value, isEarlyValidation: true); // Validate early if possible
                        continueOnError = stepProperty.Value.AssertScalar($"{WorkflowTemplateConstants.Steps} {WorkflowTemplateConstants.ContinueOnError}");
                        break;

                    case WorkflowTemplateConstants.Env:
                        ConvertToStepEnvironment(context, stepProperty.Value, StringComparer.Ordinal, isEarlyValidation: true); // Validate early if possible
                        env = stepProperty.Value;
                        break;

                    case WorkflowTemplateConstants.Id:
                        id = stepProperty.Value.AssertString($"{WorkflowTemplateConstants.Steps} item {WorkflowTemplateConstants.Id}");
                        if (!String.IsNullOrEmpty(id.Value) &&
                            !idBuilder.TryAddKnownId(id.Value, out var error))
                        {
                            context.Error(id, error);
                        }
                        break;

                    case WorkflowTemplateConstants.If:
                        ifToken = stepProperty.Value.AssertScalar($"{WorkflowTemplateConstants.Steps} item {WorkflowTemplateConstants.If}");
                        break;

                    case WorkflowTemplateConstants.Name:
                        name = stepProperty.Value.AssertScalar($"{WorkflowTemplateConstants.Steps} item {WorkflowTemplateConstants.Name}");
                        break;

                    case WorkflowTemplateConstants.Run:
                        run = stepProperty.Value.AssertScalar($"{WorkflowTemplateConstants.Steps} item {WorkflowTemplateConstants.Run}");
                        break;

                    case WorkflowTemplateConstants.Shell:
                        shell = stepProperty.Value.AssertScalar($"{WorkflowTemplateConstants.Steps} item {WorkflowTemplateConstants.Shell}");
                        break;

                    case WorkflowTemplateConstants.TimeoutMinutes:
                        ConvertToStepTimeout(context, stepProperty.Value, isEarlyValidation: true); // Validate early if possible
                        timeoutMinutes = stepProperty.Value.AssertScalar($"{WorkflowTemplateConstants.Steps} item {WorkflowTemplateConstants.TimeoutMinutes}");
                        break;

                    case WorkflowTemplateConstants.Uses:
                        uses = stepProperty.Value.AssertString($"{WorkflowTemplateConstants.Steps} item {WorkflowTemplateConstants.Uses}");
                        break;

                    case WorkflowTemplateConstants.With:
                        ConvertToStepInputs(context, stepProperty.Value, isEarlyValidation: true); // Validate early if possible
                        with = stepProperty.Value;
                        break;

                    case WorkflowTemplateConstants.WorkingDirectory:
                        workingDir = stepProperty.Value.AssertScalar($"{WorkflowTemplateConstants.Steps} item {WorkflowTemplateConstants.WorkingDirectory}");
                        break;

                    default:
                        propertyName.AssertUnexpectedValue($"{WorkflowTemplateConstants.Steps} item key"); // throws
                        break;
                }
            }

            // Fixup the if-condition
            ifCondition = ConvertToIfCondition(context, ifToken, IfKind.Step);

            if (run != null)
            {
                return new RunStep
                {
                    Id = id?.Value,
                    ContinueOnError = continueOnError,
                    Name = name,
                    If = ifCondition,
                    TimeoutMinutes = timeoutMinutes,
                    Env = env,
                    WorkingDirectory = workingDir,
                    Shell = shell,
                    Run = run,
                };
            }
            else
            {
                uses.AssertString($"{WorkflowTemplateConstants.Steps} item {WorkflowTemplateConstants.Uses}");
                var result = new ActionStep
                {
                    Id = id?.Value,
                    ContinueOnError = continueOnError,
                    Name = name,
                    If = ifCondition,
                    TimeoutMinutes = timeoutMinutes,
                    Env = env,
                    Uses = uses,
                    With = with,
                };

                if (!uses.Value.StartsWith(WorkflowTemplateConstants.DockerUriPrefix, StringComparison.Ordinal) &&
                    !uses.Value.StartsWith("./") &&
                    !uses.Value.StartsWith(".\\"))
                {
                    var usesSegments = uses.Value.Split('@');
                    var pathSegments = usesSegments[0].Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
                    var gitRef = usesSegments.Length == 2 ? usesSegments[1] : String.Empty;

                    if (usesSegments.Length != 2 ||
                        pathSegments.Length < 2 ||
                        String.IsNullOrEmpty(pathSegments[0]) ||
                        String.IsNullOrEmpty(pathSegments[1]) ||
                        String.IsNullOrEmpty(gitRef))
                    {
                        context.Error(uses, $"Expected format {{org}}/{{repo}}[/path]@ref. Actual '{uses.Value}'");
                    }
                }

                return result;
            }
        }

        /// <summary>
        /// When empty, default to "success()".
        /// When a status function is not referenced, format as "success() &amp;&amp; &lt;CONDITION&gt;".
        /// </summary>
        private static BasicExpressionToken ConvertToIfCondition(
            TemplateContext context,
            TemplateToken token,
            IfKind ifKind)
        {
            String condition;
            if (token is null)
            {
                condition = null;
            }
            else if (token is BasicExpressionToken expressionToken)
            {
                condition = expressionToken.Expression;
            }
            else
            {
                var stringToken = token.AssertString($"{ifKind} {WorkflowTemplateConstants.If}");
                condition = stringToken.Value;
            }

            if (String.IsNullOrWhiteSpace(condition))
            {
                return new BasicExpressionToken(token?.FileId, token?.Line, token?.Column, $"{WorkflowTemplateConstants.Success}()");
            }

            var expressionParser = new ExpressionParser();
            var functions = default(IFunctionInfo[]);
            var namedValues = default(INamedValueInfo[]);
            switch (ifKind)
            {
                case IfKind.Job:
                    namedValues = s_jobIfNamedValues;
                    functions = s_jobConditionFunctions;
                    break;
                case IfKind.Step:
                    namedValues = s_stepNamedValues;
                    functions = s_stepConditionFunctions;
                    break;
                case IfKind.Snapshot:
                    namedValues = s_snapshotIfNamedValues;
                    functions = s_snapshotConditionFunctions;
                    break;
                default:
                    throw new ArgumentException($"Unexpected IfKind Enum value '{ifKind}' encountered while translating the token '{token}' to an IfCondition.");
            }

            var node = default(ExpressionNode);
            try
            {
                node = expressionParser.CreateTree(condition, null, namedValues, functions, allowCaseFunction: context.AllowCaseFunction) as ExpressionNode;
            }
            catch (Exception ex)
            {
                context.Error(token, ex);
                return null;
            }

            if (node == null)
            {
                return new BasicExpressionToken(token?.FileId, token?.Line, token?.Column, $"{WorkflowTemplateConstants.Success}()");
            }

            var hasStatusFunction = node.Traverse().Any(x =>
            {
                if (x is Function function)
                {
                    return String.Equals(function.Name, WorkflowTemplateConstants.Always, StringComparison.OrdinalIgnoreCase) ||
                        String.Equals(function.Name, WorkflowTemplateConstants.Cancelled, StringComparison.OrdinalIgnoreCase) ||
                        String.Equals(function.Name, WorkflowTemplateConstants.Failure, StringComparison.OrdinalIgnoreCase) ||
                        String.Equals(function.Name, WorkflowTemplateConstants.Success, StringComparison.OrdinalIgnoreCase);
                }

                return false;
            });

            var finalCondition = hasStatusFunction ? condition : $"{WorkflowTemplateConstants.Success}() && ({condition})";
            return new BasicExpressionToken(token?.FileId, token?.Line, token?.Column, finalCondition);
        }


        private static Permissions ConvertToPermissions(TemplateContext context, TemplateToken token)
        {
            if (token is StringToken)
            {
                var permissionLevel = PermissionLevel.NoAccess;
                var permissionsStr = token.AssertString("permissions");
                switch (permissionsStr.Value)
                {
                    case "read-all":
                        permissionLevel = PermissionLevel.Read;
                        break;
                    case "write-all":
                        permissionLevel = PermissionLevel.Write;
                        break;
                    default:
                        permissionsStr.AssertUnexpectedValue(permissionsStr.Value);
                        break;
                }
                return new Permissions(permissionLevel, includeIdToken: true, includeAttestations: true, includeModels: context.GetFeatures().AllowModelsPermission);
            }

            var mapping = token.AssertMapping("permissions");
            var permissions = new Permissions();
            foreach (var pair in mapping)
            {
                var key = pair.Key.AssertString("permissions.key");
                var permissionLevel = ConvertToPermissionLevel(context, pair.Value);
                switch (key.Value)
                {
                    case "actions":
                        permissions.Actions = permissionLevel;
                        break;
                    case "artifact-metadata":
                        permissions.ArtifactMetadata = permissionLevel;
                        break;
                    case "attestations":
                        permissions.Attestations = permissionLevel;
                        break;
                    case "checks":
                        permissions.Checks = permissionLevel;
                        break;
                    case "contents":
                        permissions.Contents = permissionLevel;
                        break;
                    case "deployments":
                        permissions.Deployments = permissionLevel;
                        break;
                    case "issues":
                        permissions.Issues = permissionLevel;
                        break;
                    case "discussions":
                        permissions.Discussions = permissionLevel;
                        break;
                    case "packages":
                        permissions.Packages = permissionLevel;
                        break;
                    case "pages":
                        permissions.Pages = permissionLevel;
                        break;
                    case "pull-requests":
                        permissions.PullRequests = permissionLevel;
                        break;
                    case "repository-projects":
                        permissions.RepositoryProjects = permissionLevel;
                        break;
                    case "statuses":
                        permissions.Statuses = permissionLevel;
                        break;
                    case "security-events":
                        permissions.SecurityEvents = permissionLevel;
                        break;
                    case "id-token":
                        if (context.GetFeatures().IdToken)
                        {
                            permissions.IdToken = permissionLevel;
                        }
                        else
                        {
                            context.Error(key, $"The key 'id-token' is not allowed");
                        }
                        break;
                    case "models":
                        if (context.GetFeatures().AllowModelsPermission)
                        {
                            if (permissionLevel == PermissionLevel.Write)
                            {
                                permissions.Models = PermissionLevel.Read;
                            }
                            else
                            {
                                permissions.Models = permissionLevel;
                            }
                        }
                        else
                        {
                            context.Error(key, $"The permission 'models' is not allowed");
                        }
                        break;
                    default:
                        break;
                }
            }

            return permissions;
        }

        private static PermissionLevel ConvertToPermissionLevel(
            TemplateContext context,
            TemplateToken token)
        {
            var level = token.AssertString("permissions.value");
            switch (level.Value)
            {
                case "none":
                    return PermissionLevel.NoAccess;
                case "read":
                    return PermissionLevel.Read;
                case "write":
                    return PermissionLevel.Write;
                default:
                    level.AssertUnexpectedValue(level.Value);
                    return PermissionLevel.NoAccess;
            }
        }

        private static void ValidateWorkflowJobTrigger(
            TemplateContext context,
            ReusableWorkflowJob workflowJob)
        {
            ConvertToWorkflowJobInputs(context, workflowJob.InputDefinitions, workflowJob.InputValues, workflowJob, isEarlyValidation: true);
            ValidateWorkflowJobSecrets(context, workflowJob.SecretDefinitions, workflowJob.SecretValues, workflowJob);
        }

        private static ExpressionData ConvertToInputValueDefinedType(
            TemplateContext context,
            string key,
            StringToken definedType,
            TemplateToken token,
            Boolean isEarlyValidation = false)
        {
            var inputType = default(string);

            switch (definedType.Value)
            {
                case WorkflowTemplateConstants.TypeBoolean:
                    inputType = WorkflowTemplateConstants.BooleanNeedsContext;

                    break;
                case WorkflowTemplateConstants.TypeNumber:
                    inputType = WorkflowTemplateConstants.NumberNeedsContext;

                    break;
                case WorkflowTemplateConstants.TypeString:
                    inputType = WorkflowTemplateConstants.StringNeedsContext;

                    break;
                default:
                    // The schema for worflow_call.inputs only allows boolean, string, or number.
                    // We should have failed earlier if we receive any other type.
                    throw new ArgumentException($"Unexpected defined type '{definedType.Value}' when converting input value for '{key}'");
            }

            // Leverage the templating library to coerce or error
            //
            // During early validation, we're not actually evaluating any expressions with this call.
            // Any allowed contexts (i.e. "github"/"inputs") have not been added yet, so the TemplateEvaluator
            // will not unravel any expressions.
            //
            // During runtime, the expressions have already been expanded.
            var result = TemplateEvaluator.Evaluate(
                context,
                inputType,
                token,
                context.Memory.CalculateBytes(token), // Remove the size of the template token that is being replaced
                token.FileId
            );

            if (isEarlyValidation && token.Traverse().Any(x => x is ExpressionToken))
            {
                return null;
            }

            return result.ToExpressionData();
        }

        internal static IDictionary<String, String> ConvertToWorkflowJobOutputs(TemplateToken workflowJobOutputDefinitions)
        {
            var result = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
            var outputs = workflowJobOutputDefinitions
                .AssertMapping("workflow job output definitions")
                .ToDictionary(
                    x => x.Key.AssertString("outputs key").Value,
                    x => x.Value,
                    StringComparer.OrdinalIgnoreCase
                );

            foreach (var definition in outputs)
            {
                var spec = definition.Value.AssertMapping("workfow job output spec").ToDictionary(
                    x => x.Key.AssertString("outputs spec key").Value,
                    x => x.Value,
                    StringComparer.OrdinalIgnoreCase
                );

                var value = spec["value"].AssertString("workfow job output value").Value;

                result.Add(definition.Key, value);
            }

            return result;
        }

        internal static DictionaryExpressionData ConvertToWorkflowJobInputs(
            TemplateContext context,
            TemplateToken workflowJobInputDefinitions,
            TemplateToken workflowJobInputValues,
            ReusableWorkflowJob workflowJob,
            Boolean isEarlyValidation = false)
        {
            var result = default(DictionaryExpressionData);
            var inputDefinitions = workflowJobInputDefinitions?
                .AssertMapping("workflow job input definitions")
                .ToDictionary(
                    x => x.Key.AssertString("inputs key").Value,
                    x => x.Value,
                    StringComparer.OrdinalIgnoreCase
                );

            var inputValues = workflowJobInputValues?
                .AssertMapping("workflow job input values")
                .ToDictionary(
                    x => x.Key.AssertString("with key").Value,
                    x => x.Value,
                    StringComparer.OrdinalIgnoreCase
                );

            if (inputDefinitions != null)
            {
                result = new DictionaryExpressionData();
                foreach (var definedItem in inputDefinitions)
                {
                    string definedKey = definedItem.Key;
                    var definedInputSpec = definedItem.Value.AssertMapping($"input {definedKey}").ToDictionary(
                            x => x.Key.AssertString($"input {definedKey} key").Value,
                            x => x.Value,
                            StringComparer.OrdinalIgnoreCase);

                    var inputSpecType = definedInputSpec[WorkflowTemplateConstants.Type].AssertString($"inputs {definedKey} type"); // must exist, per schema

                    // if default provided, check with the defined type
                    if (definedInputSpec.TryGetValue(WorkflowTemplateConstants.Default, out TemplateToken defaultValue))
                    {
                        var value = ConvertToInputValueDefinedType(context, definedKey, inputSpecType, defaultValue, isEarlyValidation);
                        if (!isEarlyValidation)
                        {
                            result.Add(definedKey, value);
                        }
                    }
                    else if (!isEarlyValidation)
                    {
                        result.Add(definedKey, GetDefaultValueByType(inputSpecType).ToExpressionData());
                    }

                    // if input provided, check with defined type and continue
                    if (inputValues != null && inputValues.TryGetValue(definedKey, out TemplateToken inputValue))
                    {
                        var value = ConvertToInputValueDefinedType(context, definedKey, inputSpecType, inputValue, isEarlyValidation);
                        if (!isEarlyValidation)
                        {
                            result[definedKey] = value;
                        }
                        continue;
                    }

                    // if input required but not provided, error out
                    if (isEarlyValidation
                        && definedInputSpec.TryGetValue(WorkflowTemplateConstants.Required, out TemplateToken requiredToken)
                        && requiredToken.AssertBoolean(WorkflowTemplateConstants.Required).Value)
                    {
                        context.Error(workflowJob.Ref, $"Input {definedKey} is required, but not provided while calling.");
                        continue;
                    }
                }
            }

            // Validating if any undefined inputs are provided
            ValidateUndefinedParameters(context, inputDefinitions, inputValues, "input");

            return result;
        }

        private static void ValidateWorkflowJobSecrets(
           TemplateContext context,
           MappingToken workflowJobSecretDefinitions,
           MappingToken workflowJobSecretValues,
           ReusableWorkflowJob workflowJob)
        {
            // if the secrets are inherited from the caller, we do not have any workflowJob.SecretValues (i.e. explicit mapping)
            // Inherited org/repo/env secrets will be stored in context variables and will be validated there
            if (workflowJob.InheritSecrets)
            {
                return;
            }

            var secretDefinitions = workflowJobSecretDefinitions?.ToDictionary(
                x => x.Key.AssertString("secrets key").Value,
                x => x.Value, StringComparer.OrdinalIgnoreCase);

            var secretValues = workflowJobSecretValues?.ToDictionary(
                x => x.Key.AssertString("secrets key").Value,
                x => x.Value, StringComparer.OrdinalIgnoreCase);

            if (secretDefinitions != null)
            {
                foreach (var definedItem in secretDefinitions)
                {
                    if (definedItem.Value is NullToken nullToken)
                    {
                        continue;
                    }

                    var definedKey = definedItem.Key.ToString();
                    var definedSecretSpec = definedItem.Value.AssertMapping($"secret {definedKey}").ToDictionary(
                            x => x.Key.AssertString($"secret {definedKey} key").Value,
                            x => x.Value, StringComparer.OrdinalIgnoreCase);

                    // if secret provided, continue
                    if (secretValues != null && secretValues.TryGetValue(definedKey, out TemplateToken secretValue))
                    {
                        continue;
                    }

                    // if secret required but not provided, error out
                    if (definedSecretSpec.TryGetValue(WorkflowTemplateConstants.Required, out TemplateToken requiredToken)
                    && requiredToken.AssertBoolean(WorkflowTemplateConstants.Required).Value)
                    {
                        context.Error(workflowJob.Ref, $"Secret {definedKey} is required, but not provided while calling.");
                    }
                }
            }

            // Validating if any undefined secrets are provided
            ValidateUndefinedParameters(context, secretDefinitions, secretValues, WorkflowTemplateConstants.Secret);
        }

        private static void ValidateUndefinedParameters(
           TemplateContext context,
           Dictionary<string, TemplateToken> definitions,
           Dictionary<string, TemplateToken> providedValues,
           string parameterType)
        {
            if (providedValues != null)
            {
                foreach (var providedValue in providedValues)
                {
                    var providedKey = providedValue.Key;
                    if (definitions == null || !definitions.TryGetValue(providedKey, out TemplateToken value))
                    {
                        context.Error(providedValue.Value, $"Invalid {parameterType}, {providedKey} is not defined in the referenced workflow.");
                    }
                }
            }
        }

        private static TemplateToken GetDefaultValueByType(StringToken type)
        {
            return type.Value switch
            {
                WorkflowTemplateConstants.TypeString => new StringToken(type.FileId, type.Line, type.Column, string.Empty),
                WorkflowTemplateConstants.TypeBoolean => new BooleanToken(type.FileId, type.Line, type.Column, false),
                WorkflowTemplateConstants.TypeNumber => new NumberToken(type.FileId, type.Line, type.Column, 0.0),
                _ => null,
            };
        }

        private sealed class NodeInfo
        {
            public String Name { get; set; }
            public List<StringToken> Needs { get; set; }
        }

        private enum IfKind
        {
            Job = 0,
            Step = 1,
            Snapshot = 2
        }
        private static readonly INamedValueInfo[] s_jobIfNamedValues = new INamedValueInfo[]
        {
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.GitHub),
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Vars),
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Inputs),
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Needs),
        };
        private static readonly INamedValueInfo[] s_stepNamedValues = new INamedValueInfo[]
        {
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.GitHub),
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Vars),
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Inputs),
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Needs),
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Strategy),
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Matrix),
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Steps),
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Job),
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Runner),
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Env),
        };
        private static readonly INamedValueInfo[] s_snapshotIfNamedValues = new INamedValueInfo[]
        {
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.GitHub),
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Vars),
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Inputs),
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Needs),
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Strategy),
            new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Matrix),
        };
        private static readonly IFunctionInfo[] s_jobConditionFunctions = new IFunctionInfo[]
        {
            new FunctionInfo<NoOperation>(WorkflowTemplateConstants.Always, 0, 0),
            new FunctionInfo<NoOperation>(WorkflowTemplateConstants.Failure, 0, Int32.MaxValue),
            new FunctionInfo<NoOperation>(WorkflowTemplateConstants.Cancelled, 0, 0),
            new FunctionInfo<NoOperation>(WorkflowTemplateConstants.Success, 0, Int32.MaxValue),
        };
        private static readonly IFunctionInfo[] s_stepConditionFunctions = new IFunctionInfo[]
        {
            new FunctionInfo<NoOperation>(WorkflowTemplateConstants.Always, 0, 0),
            new FunctionInfo<NoOperation>(WorkflowTemplateConstants.Cancelled, 0, 0),
            new FunctionInfo<NoOperation>(WorkflowTemplateConstants.Failure, 0, 0),
            new FunctionInfo<NoOperation>(WorkflowTemplateConstants.Success, 0, 0),
            new FunctionInfo<NoOperation>(WorkflowTemplateConstants.HashFiles, 1, Byte.MaxValue),
        };
        private static readonly IFunctionInfo[] s_snapshotConditionFunctions = null;
    }
}
