What you need is a "valid map" to determine which stages can be gotten to from which other stage. You don't need any complicated if statements with such a setup.
Here's a hard-coded example:
Map<String, List<String>> validChanges = new Map<String, List<String>> {
'A' => new String[] { 'B' },
'B' => new String[] { 'A', 'C' },
// ... more mappings ... //
};
Once you have this all mapped out, you can reduce your main logic to just:
for(Integer i = 0, s = Trigger.size; i < s; i++) {
String oldStatus = Trigger.old[i].StageName, newStatus = Trigger.new[i].StageName;
String[] validStatusChanges = validChanges.get(oldStatus);
if(oldStatus != newStatus && !validStatusChanges.contains(newStatus)) {
Trigger.new[i].StageName.addError(
'You cannot move from '+oldStatus+' to '+newStatus+
'. Valid changes are: '+String.join(validStatusChanges,', '));
}
}
(Note, you could write it other ways, I just find this seems to read pretty easy).
Even better, this allows you to specifically tell the user which valid changes they may make.
If you wanted to make this not hard-coded, you could use a Custom Metadata object to store the valid mappings, and decode that during runtime.
Map<String, List<String>> validChanges = new Map<String, List<String>>();
for(StageChange__mdt change: StageChange__mdt.getAll().values()) {
String[] stageChanges = validChanges.get(change.OldStage__c);
if(stageChanges == null) {
validChanges.put(change.OldStage__c, stageChanges = new String[0]);
}
stageChanges.add(change.NewStage__c);
}
This requires a bit more setup, but allows you to change the legal steps without updating any code at all.
Sometimes, the best solution to a problem isn't to try and attack it head-on, but to find an easier method. Hard-coding a list of values in a huge if-else chain is just asking for trouble. Not only do you have to deal with maintaining code, but code coverage for a head-on attack would be challenging compared to more dynamic methods.