function [TestCases, Objectives, ...
          obsOutput, testCaseReqTableMap] = addTestCase(testCase, sampleTime,...
                                                        TestCases, ...
                                                        Objectives, obsInput, ...
                                                        goalIdToObjectiveIdMap,...
                                                        modelH, testcomp,...
                                                        tcIdxToSimoutMap, ...
                                                        checkGoalTestCaseMapping, ...
                                                        testCaseReqTableMap)
%

% Converts a UDD testcase ('tc') to sldvData testcase and appends it to existing 'TestCases'
%
% Inputs:
% -------
% 1. testCase                   - UDD testcase
% 2. sampleTime                 - Sample time (step size)
% 3. TestCases                  - Existing testcases in sldvData format
% 4. Objectives                 - Objectives (goals) for analysis in sldvData format
% 5. obsInput                   - Observability Information
%                               1. PathObjectives
%                               2. extObjectives
%                               3. goalIdToExtObjIdMap
%                               4. goalIdToPathObjIdMap
% 6. goalIdToObjectiveIdMap     - Goal-id to Objective-id mapping
% 7. modelH                     - Design model handle
% 8. testcomp                   - TestComponent
% 9. tcIdxToSimoutMap           -
% 10.checkGoalTestCaseMapping   - The objectives get attached to the
%                                 testcase only if the corresponding goal
%                                 points to the same testCase.
% 11.testCaseReqTableMap        - Cell array of the form
%                                 {{testcaseId, [positiveDecisionObjectiveIdsFromRequirementsTableBlocks]}; ...}
%
% (Need: A goal can be attached to several testcases but points to a single testcase
% During validation, it is desirable to include the goal as it results in 
% a potential testcase. 
% During result generation, the testcase pointed by the goal is the better
% testcase and hence attach the objective only if the goal points to that
% testcase.)
%
% Outputs:
% --------
% 1. TestCases              - Appended vector of testcases in sldvData format
% 2. Objectives             - Objectives updated with analysis status and testcase details
% 3. obsOutput              - Updated Observability Information
%                               1. PathObjectives - with TC information
%                                                   updated
%                               2. extObjectives - with TC information
%                                                   updated
%                               3. goalIdToExtObjIdMap
%                               4. goalIdToPathObjIdMap
% 4. testCaseReqTableMap    - Updated input argument of the same name
%
% The goalIdToObjectiveIdMap won't have any disabled & internal goals. As a
% result of this no testcases with internal/disabled will be added
% SldvData.

%   Copyright 2017-2023 The MathWorks, Inc.

    if nargin < 9
        tcIdxToSimoutMap = [];
    end
    if nargin < 10
        checkGoalTestCaseMapping = false;
    end
    [timeValues, dataValues,...
        dataNoEffect, paramValues,...
        stepValues, simulationOut] = getDataValue(testcomp, testCase, sampleTime, modelH, testcomp.mdlFlatIOInfo);
    
    testCaseId = testCase.getId;
    reqTableDecObjIdxs = testCase.reqTableTrueDecObjIdxs;
    objectives = [];
    pathobjectives = [];
    extObjsStruct = [];
    obsOutput = obsInput;

    % initialize the Objectives structure with testCaseIdx field
    if ~isempty(Objectives) && ~(isfield(Objectives, 'testCaseIdx'))
        Objectives(1).testCaseIdx = [];
    end
    
    if ~isempty(obsInput)
        PathObjectives = obsInput.PathObjectives;
        extObjectives = obsInput.extObjectives;
        goalIdToExtObjIdMap = obsInput.goalIdToExtObjIdMap;
        goalIdToPathObjIdMap = obsInput.goalIdToPathObjIdMap;
    else
        PathObjectives = [];
        extObjectives= [];
        goalIdToExtObjIdMap = [];
        goalIdToPathObjIdMap= [];
    end
    if ~isempty(PathObjectives) && ~(isfield(PathObjectives, 'testCaseIdx'))
        PathObjectives(1).testCaseIdx = [];
    end
    
    if ~isempty(extObjectives) && ~(isfield(extObjectives, 'testCaseIdx'))
        extObjectives(1).testCaseIdx = [];
    end
    
    % Add some heuristics to determine if the test case is valid as a way
    % to work around prover bugs

    if isempty(timeValues) || (isempty(dataValues) && isempty(paramValues))
        return;
    end

    goals = testCase.goals;
    newTestCaseIdx = length(TestCases) + 1;

    stopped = false;
    while ~stopped
        for goal = goals(:)'
            if checkGoalTestCaseMapping && (goal.testIndex~= testCase.getId())
                continue;
            end
            if strcmp(goal.type,  'AVT_GOAL_PATH_OBJECTIVE')
                objStruct = getObjTCStruct(goalIdToPathObjIdMap,...
                                                        goal, PathObjectives,...
                                                        testCase, sampleTime);
                if ~isempty(objStruct)
                    objIdx = objStruct.objectiveIdx;
                    if strcmp(PathObjectives(objIdx).status, 'Satisfied')
                        PathObjectives(objIdx).status = goal.statusstr;
                    elseif strcmp(PathObjectives(objIdx).status, 'Undecided due to stubbing')
                        PathObjectives(objIdx).status = 'Active Logic';
                        continue;
                    end
                    PathObjectives(objIdx).testCaseIdx = newTestCaseIdx;
                    if isempty(pathobjectives)
                        pathobjectives = objStruct;
                    else
                        pathobjectives(end+1) = objStruct; %#ok<AGROW>
                    end
                end
                
            elseif strcmp(goal.type,  'AVT_GOAL_CUSBLKCOV')
                objStruct = getObjTCStruct(goalIdToExtObjIdMap,...
                                                        goal, extObjectives,...
                                                        testCase, sampleTime);
                if ~isempty(objStruct)
                    objIdx = objStruct.objectiveIdx;
                    if strcmp(extObjectives(objIdx).status, 'Satisfied')
                        extObjectives(objIdx).status = goal.statusstr;
                    elseif strcmp(extObjectives(objIdx).status, 'Undecided due to stubbing')
                        extObjectives(objIdx).status = 'Active Logic';
                        continue;
                    end
                    extObjectives(objIdx).testCaseIdx = newTestCaseIdx;
                    if isempty(extObjsStruct)
                        extObjsStruct = objStruct;
                    else
                        extObjsStruct(end+1) = objStruct; %#ok<AGROW>
                    end
                end
            else   
                 objStruct = getObjTCStruct(goalIdToObjectiveIdMap,...
                                                        goal, Objectives,...
                                                        testCase, sampleTime);
                 if ~isempty(objStruct)
                     objIdx = objStruct.objectiveIdx;
                     if strcmp(Objectives(objIdx).status, 'Satisfied')
                         Objectives(objIdx).status = goal.statusstr;
                     elseif strcmp(Objectives(objIdx).status, 'Undecided due to stubbing')
                         Objectives(objIdx).status = 'Active Logic';
                         continue;
                     elseif (isequal(goal.type, 'AVT_GOAL_DESRANGE') || ...
                             isequal(goal.type, 'AVT_GOAL_ARRBOUNDS')) && ...
                             isfield(Objectives(objIdx), 'testCaseIdx') && ...
                             ~isempty(Objectives(objIdx).testCaseIdx)
                         continue;
                     end
                     Objectives(objIdx).testCaseIdx = newTestCaseIdx;
                     if isempty(objectives)
                         objectives = objStruct;
                     else                     
                         objectives(end+1) = objStruct; %#ok<AGROW>
                     end
                 end             
            end
        end

        stopped = ~isa(testCase.up, 'sldv.datamodel.mfzero.TestCase');
        testCase = testCase.up;
        if ~stopped
            goals = testCase.goals;
        end
    end

    if checkGoalTestCaseMapping
        toAdd = ~isempty(objectives);
    else
        toAdd = ~isempty(objectives) ||...
                    ~isempty(pathobjectives) || ...
                    ~isempty(extObjsStruct);
    end

    if toAdd
        errorDescr = [];
        tc = testcomp.getTestCase(testCaseId);
        if isprop(tc.simoutData, 'SimulationMetadata')
            if isprop(tc.simoutData.SimulationMetadata, 'ExecutionInfo')
                if isfield(tc.simoutData.SimulationMetadata.ExecutionInfo, 'ErrorDiagnostic')
                    if isfield(tc.simoutData.SimulationMetadata.ExecutionInfo.ErrorDiagnostic, 'Diagnostic')                                     
                        errorDescr.ErrorDiagnostic = deleteStackField(tc.simoutData.SimulationMetadata.ExecutionInfo.ErrorDiagnostic.Diagnostic);
                        errorDescr.ErrorId = tc.simoutData.SimulationMetadata.ExecutionInfo.ErrorDiagnostic.Diagnostic.identifier;                        
                        errorDescr.ErrorMessageArgs = Sldv.utils.getMessageIDArgs(errorDescr.ErrorId, errorDescr.ErrorDiagnostic.message);                           
                    end
                end
            end
        end   
        
        newTestCase = struct(...
            'timeValues', timeValues,...
            'dataValues', '',...
            'paramValues', paramValues, ...
            'stepValues', stepValues,...
            'objectives', objectives,...
            'testCaseId', testCaseId,...
            'errorDescr', errorDescr...
            );
        
        if Sldv.utils.isPathBasedTestGeneration(testcomp.activeSettings)
            newTestCase.extObjectives = extObjsStruct;
            newTestCase.pathobjectives = pathobjectives;
            obsOutput.PathObjectives = PathObjectives;
            obsOutput.extObjectives = extObjectives;
        end

        newTestCase.dataValues = dataValues;
        newTestCase.dataNoEffect = dataNoEffect;

        if isempty(TestCases)
            TestCases = newTestCase;
        else
            TestCases(end+1) = newTestCase;
        end

        if isa(tcIdxToSimoutMap, 'containers.Map')
            tcIdxToSimoutMap(length(TestCases)) = simulationOut;
        end

        if ~isempty(reqTableDecObjIdxs)
            testCaseReqTableMap{end+1} = {newTestCaseIdx, reqTableDecObjIdxs};
        end
    end
end

function objStruct = getObjTCStruct(goalIdToObjIdMap, goal,...
                                            objectives, testCase, sampleTime)
    objStruct = [];
    if ~isempty(goalIdToObjIdMap) &&...
            goalIdToObjIdMap.isKey(goal.getGoalMapId)
        objIdx = goalIdToObjIdMap(goal.getGoalMapId);
        if isempty(objectives(objIdx).testCaseIdx)
            % If the objective is not already satisfied by a
            % test-case then add this objective to the collection
            % of objectives satisfied and also mark the
            % test-case for addition by setting toAdd to true
            % If an objective is satisfied by multiple
            % test-cases, then we select the first test-case to
            % report.
            % If a test-case only satisfies already satisfied
            % objectives, then it is discarded.

            % Ideally atStep should be saved as 'int' in sldvData,
            % but keeping this as 'double', as sldvData clients expect it in 'double' format.
            atStep = double(testCase.satisfiedDepth(goal));
            % atTime array has to be of type double as sampleTime can be a fraction
            atTime = double(atStep-1)*sampleTime;
            if atStep~=0  % A value of 0 indicates some type of error with the goal.
                objStruct = struct('objectiveIdx', objIdx, 'atTime', atTime, ...
                    'atStep', atStep);   
            end
        end
    end
end
function [time, data, noEffect, params, step, simulationOut] = getDataValue(testcomp, tc, ts, modelH, parameterInfo)
    time = [];
    data = {};
    params = [];
    step = [];
    noEffect = {};
    simulationOut = tc.simoutData;
    rawData = tc.inData;
    parameters = parameterInfo.Parameter;
    paramMap = parameterInfo.ParamNameMap;
    if (isempty(rawData) || (iscell(rawData) && isempty(rawData{1}))) && isempty(tc.params)
        return;
    else
        data = rawData;
        noEffect = tc.dataNoEffect;
    end

    % Ideally stepCnt should be saved as 'int' in sldvData,
    % but keeping this as 'double', as sldvData clients expect it in 'double' format.
    stepCnt = double(tc.length);

    params = tc.params;
    if slavteng('feature','BusParameterTuning')
        [parameterData, fieldIdMap] = sldvprivate('createLeafParameterData',parameters);
        noOfParamEntries = length(parameters);
    else
        noOfParamEntries = length(params);
    end

    import Sldv.utils.SLDVParamType;

    paramDefaultStruct = struct('name', '',...
                                'value', [], ...
                                'noEffect', 1, ...
                                'type', char(SLDVParamType(0)));

    paramsConsolidated(1:noOfParamEntries) = paramDefaultStruct;
    modelName = get_param(modelH, 'name');

    % g2733274: We are unable to resolve the parameters present in DD of
    % referenced model correctly
    % TopTester: matlab/test/toolbox/sldv/reuseTranslation/tAcrossSession_Basic.m -testspec tCreateReplacementDD_OpenDD
    dataAccessor = Simulink.data.DataAccessor.createForGlobalNameSpaceClosure(modelName);

    startupParamMap = getStartupParamMap(testcomp);

    for idx=1:length(params)       
        if slavteng('feature','BusParameterTuning')
            params(idx).name = parameterData(idx).paramNameStr;
            params(idx).value = Sldv.DataUtils.reshapeData(1, parameterData(idx).Dimensions, params(idx).value);
        end
        % SLDV is source agnostic in retrieving type definitions, be it bus
        % or param or anything else
        paramName = params(idx).name;
        topLvlParam = [];

        if contains(params(idx).name, '.')
            paramNameArr =  strsplit(params(idx).name, '.');
            paramIdx = paramMap(paramNameArr{1});
            SourceType = parameters(paramIdx).SourceType;
            Source = parameters(paramIdx).Source;
            paramVarId = dataAccessor.identifyByName(paramNameArr{1});
            topLvlParam = dataAccessor.getVariable(paramVarId);
            if isa(topLvlParam, 'Simulink.Parameter')
                paramName = ['topLvlParam' '.Value.' strjoin(paramNameArr(2:end),'.')];
            elseif isa(topLvlParam, 'struct')
                paramName = ['topLvlParam.' strjoin(paramNameArr(2:end),'.')];
            end
            baseParamVal = eval(paramName);
        else
            paramIdx = paramMap(paramName);
            SourceType = parameters(paramIdx).SourceType;
            Source = parameters(paramIdx).Source;
            baseParamVal = sldvshareprivate('getVariableFromWorkspace', SourceType, Source, paramName, dataAccessor);
        end

        [isEnum, enumCls] = sldvshareprivate('util_is_enum_type', class(baseParamVal));
        if(isEnum)
            params(idx).value = feval(enumCls, params(idx).value);
        end
        params(idx).noEffect = logical(params(idx).noEffect);
        if slavteng('feature','BusParameterTuning')
            paramIdx = fieldIdMap(parameterData(idx).fieldID);
            if isempty(paramsConsolidated(paramIdx).name)
                paramsConsolidated(paramIdx).name = parameters(paramIdx).name;
                paramsConsolidated(paramIdx).sourceType = SourceType;
                paramsConsolidated(paramIdx).source = Source;
            end
        else
            paramIdx = idx;
            paramsConsolidated(paramIdx).sourceType = SourceType;
            paramsConsolidated(paramIdx).source = Source;
        end
        if slavteng('feature','BusParameterTuning') && parameters(paramIdx).isStructBus
            % For struct params, if all of the individual elements is no
            % effect, we consider the whole struct to be no effect. In
            % all  the other cases, no effect field is set to 0
            paramsConsolidated(paramIdx).noEffect = paramsConsolidated(paramIdx).noEffect && ....
                all(params(idx).noEffect, 'all');
            paramNameArr =  strsplit(params(idx).name, '.');
            % Resultant value for struct params will have the name of the
            % parameter in the value field
            % For example, value = struct('EngineParam',struct('speed',0, 'acceleration', 0))
            % We typically would like this struct to be a name value pair that
            % we can use in an assignin statement. So, we would like to convert
            % the above to value = struct('speed',0, 'acceleration', 0).
            % This way, assignin(name, value) becomes
            % assignin('EngineParam', struct('speed',0, 'acceleration', 0))
            % Hence, we ignore the first entry in the paramNameArr
            reducedParamName = strjoin(paramNameArr(2:end), '.');
            paramsConsolidated(paramIdx).value  = assignParamValue(paramsConsolidated(paramIdx).value,...
                                                                reducedParamName, params(idx).value);
        else
            % For non struct/Bus params
            paramsConsolidated(paramIdx).noEffect = params(idx).noEffect;
            paramsConsolidated(paramIdx).value = params(idx).value;
            paramsConsolidated(paramIdx).name = params(idx).name;
        end

        paramsConsolidated(paramIdx) = updateParamType(paramsConsolidated(paramIdx), startupParamMap, dataAccessor);
    end
    step = {1:stepCnt};
    % time array has be of type double as sampleTime(ts) can be a fraction
    time = {double(step{1}-1) * ts};

    % for the cases when the there is no corresponding entry in tc.params
    % wrt to parameters present in modelFlatIO, we create empty default
    % params struct with empty name and need to remove those for
    % successful simulation
    % TopTester: test/toolbox/sldv/params/tTestsForMultisimParams.m
    idxToRemove = [];
    for idx = 1:length(paramsConsolidated)
        if (paramsConsolidated(idx).name == "")
            if isempty(idxToRemove)
                idxToRemove = idx;
            else
                idxToRemove(end+1) = idx;
            end
        end
    end
    paramsConsolidated(idxToRemove) = [];
    params = paramsConsolidated';
end

function paramStruct = assignParamValue(paramStruct, paramName, value)
    % Initialize a parameter with '[]' if it is selected for tuning
    % and it is a part of parameterStruct
    paramElems = strsplit(paramName, '.');
    topLvlParamName = paramElems{1};
    if slavteng('feature','BusParameterTuning') && length(paramElems)>1
        currentFieldName = strjoin(paramElems(2:end),'.');
        if isfield(paramStruct, topLvlParamName)
            param = paramStruct.(topLvlParamName);
        else
            paramStruct.(topLvlParamName) = [];
            param = struct;
        end
        param = assignParamValue(param, currentFieldName, value);
        paramStruct.(topLvlParamName) = param;
    else
        paramStruct.(paramName) = value;
    end
end

function startupParamMap = getStartupParamMap(testcomp)
    startupParamMap = containers.Map('KeyType', 'char', 'ValueType', 'logical');
    if isempty(testcomp.tunableParamsAndConstraints)
        return;
    end
    
    for idx = 1:numel(testcomp.tunableParamsAndConstraints.VariantParams)
        startupParamMap(testcomp.tunableParamsAndConstraints.VariantParams{idx}) = true;
    end
end

function paramsConsolidated = updateParamType(paramsConsolidated, startupParamMap, dataAccessor)
    % Currently we are only updating for variant param type
    % Check and update the paramType based on SLDVParamType ENUM
    if isequal(startupParamMap.Count, 0)
        return;
    end
    
    import Sldv.utils.SLDVParamType;
    
    paramName = paramsConsolidated.name;
    if isKey(startupParamMap, paramName)
        paramsConsolidated.type = char(SLDVParamType(1));

        % For startup variants data noEffect can not be '0' (which is the
        % default). Therefore we need to get the values from the base
        % workspace. This would make sure that we do not hit 'runtime
        % error' during validation
        if isequal(paramsConsolidated.noEffect, 1)
            paramID = dataAccessor.identifyByName(paramName);
            paramVar = dataAccessor.getVariable(paramID);
            if isa(paramVar, 'Simulink.Parameter')
                % For variant control variables of type 'Simulink.Parameter'
                % Example: myVariantVariableValue = Simulink.Parameter(1);
                paramsConsolidated.value = paramVar.Value;
            else
                % For variant control variables not of type 'Simulink.Parameter'
                % Example: myVariantVariableValue = 1;
                paramsConsolidated.value = paramVar;
            end
        end
    end
end

function mslDiagStruct = deleteStackField(mslDiag)
% MSLDiagnostic has a stack field which has source code filenames 
% which we cannot expose. So deleting this field recursively.
mslDiagStruct.identifier = mslDiag.identifier;
mslDiagStruct.message = mslDiag.message;
mslDiagStruct.paths = mslDiag.paths;
mslDiagStruct.cause = {};
for i=1:numel(mslDiag.cause)
    mslDiagStruct.cause{end+1} = deleteStackField(mslDiag.cause{i});
end
end