diff --git a/forge-core/src/main/java/forge/deck/DeckRecognizer.java b/forge-core/src/main/java/forge/deck/DeckRecognizer.java index fe8aee6cb53..9b3104a83ab 100644 --- a/forge-core/src/main/java/forge/deck/DeckRecognizer.java +++ b/forge-core/src/main/java/forge/deck/DeckRecognizer.java @@ -31,7 +31,6 @@ import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; -import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -49,14 +48,15 @@ public class DeckRecognizer { * The Enum TokenType. */ public enum TokenType { - KnownCard, - UnknownCard, - IllegalCard, - DeckName, - DeckSectionName, - Comment, - UnknownText, - CardType + LEGAL_CARD_REQUEST, + ILLEGAL_CARD_REQUEST, + INVALID_CARD_REQUEST, + UNKNOWN_CARD_REQUEST, + DECK_NAME, + DECK_SECTION_NAME, + COMMENT, + UNKNOWN_TEXT, + CARD_TYPE } /** @@ -68,35 +68,41 @@ public class DeckRecognizer { private final int number; private final String text; - public static Token knownCard(final PaperCard theCard, final int count) { - return new Token(theCard, TokenType.KnownCard, count, null); + public static Token KnownCard(final PaperCard theCard, final int count) { + return new Token(theCard, TokenType.LEGAL_CARD_REQUEST, count, null); } - public static Token illegalCard(final String cardName, final String setCode, final int count) { + public static Token IllegalCard(final String cardName, final String setCode, final int count) { String ttext = setCode == null || setCode.equals("") ? cardName : String.format("%s [%s]", cardName, setCode); - return new Token(null, TokenType.IllegalCard, count, ttext); + return new Token(null, TokenType.ILLEGAL_CARD_REQUEST, count, ttext); } - public static Token unknownCard(final String cardName, final String setCode, final int count) { + public static Token InvalidCard(final String cardName, final String setCode, final int count) { String ttext = setCode == null || setCode.equals("") ? cardName : String.format("%s [%s]", cardName, setCode); - return new Token(null, TokenType.UnknownCard, count, ttext); + return new Token(null, TokenType.INVALID_CARD_REQUEST, count, ttext); } - public static Token deckSection(final String sectionName){ - if (sectionName.contains("avatar")) - return new Token(TokenType.DeckSectionName, 1, DeckSection.Avatar.name()); - if (sectionName.contains("commander")) - return new Token(TokenType.DeckSectionName, 1, DeckSection.Commander.name()); - if (sectionName.contains("schemes")) - return new Token(TokenType.DeckSectionName, 1, DeckSection.Schemes.name()); - if (sectionName.contains("conspiracy")) - return new Token(TokenType.DeckSectionName, 1, DeckSection.Conspiracy.name()); - if (sectionName.contains("side") || sectionName.contains("sideboard")) - return new Token(TokenType.DeckSectionName, 1, DeckSection.Sideboard.name()); - if (sectionName.contains("main") || sectionName.contains("card")) - return new Token(TokenType.DeckSectionName, 1, DeckSection.Main.name()); + public static Token UnknownCard(final String cardName, final String setCode, final int count) { + String ttext = setCode == null || setCode.equals("") ? cardName : + String.format("%s [%s]", cardName, setCode); + return new Token(null, TokenType.UNKNOWN_CARD_REQUEST, count, ttext); + } + + public static Token DeckSection(final String sectionName){ + if (sectionName.equals("avatar")) + return new Token(TokenType.DECK_SECTION_NAME, DeckSection.Avatar.name()); + if (sectionName.equals("commander")) + return new Token(TokenType.DECK_SECTION_NAME, DeckSection.Commander.name()); + if (sectionName.equals("schemes")) + return new Token(TokenType.DECK_SECTION_NAME, DeckSection.Schemes.name()); + if (sectionName.equals("conspiracy")) + return new Token(TokenType.DECK_SECTION_NAME, DeckSection.Conspiracy.name()); + if (sectionName.equals("side") || sectionName.contains("sideboard")) + return new Token(TokenType.DECK_SECTION_NAME, DeckSection.Sideboard.name()); + if (sectionName.equals("main") || sectionName.contains("card") || sectionName.equals("mainboard")) + return new Token(TokenType.DECK_SECTION_NAME, DeckSection.Main.name()); return null; } @@ -109,11 +115,15 @@ public class DeckRecognizer { public Token(final TokenType type1, final int count, final String message) { this(null, type1, count, message); - if ((type1 == TokenType.KnownCard) || (type1 == TokenType.UnknownCard)) { + if ((type1 == TokenType.LEGAL_CARD_REQUEST) || (type1 == TokenType.UNKNOWN_CARD_REQUEST)) { throw new IllegalArgumentException("Use factory methods for recognized " + REGRP_CARD + " lines"); } } + public Token(final TokenType type1, final String message) { + this(type1, 0, message); + } + public final String getText() { return this.text; } @@ -146,38 +156,66 @@ public class DeckRecognizer { // private static final Pattern EDITION_AFTER_CARD_NAME = Pattern.compile("([\\d]{1,2})?\\s*([a-zA-Z',\\/\\-\\s]+)\\s*((\\(|\\[)([a-zA-Z0-9]{3})(\\)|\\])\\s*)?([\\d]{1,3})?"); // Core Matching Patterns (initialised in Constructor) - private static final String REGRP_DECKNAME = "deckName"; - private static final String DECK_NAME_REGEX = - String.format("(?
(deck(\\s+name)?)|((deck\\s+)?name))(\\:)?\\s*(?<%s>[a-zA-Z',\\/\\-\\s]+)\\s*",
+    public static final String REGRP_DECKNAME = "deckName";
+    public static final String REX_DECK_NAME =
+            String.format("^(//\\s*)?(?
(deck name|name|deck))(\\:|\\s)\\s*(?<%s>[a-zA-Z0-9',\\/\\-\\s]+)\\s*(.*)$",
                     REGRP_DECKNAME);
-    private static final Pattern DECK_NAME_PATTERN = Pattern.compile(DECK_NAME_REGEX, Pattern.CASE_INSENSITIVE);
+    public static final Pattern DECK_NAME_PATTERN = Pattern.compile(REX_DECK_NAME, Pattern.CASE_INSENSITIVE);
 
-    private static final String REGRP_CARDNO = "count";
-    private static final String REGRP_SET = "setCode";
-    private static final String REGRP_COLLNO = "collectorNumber";
-    private static final String REGRP_CARD = "card";
+    public static final String REGRP_NOCARD = "token";
+    public static final String REX_NOCARD = String.format("^(?
[^a-zA-Z]*)\\s*(?(\\w+[:]\\s*))?(?<%s>[a-zA-Z]+)(?<post>[^a-zA-Z]*)?$", REGRP_NOCARD);
+    public static final Pattern NONCARD_PATTERN = Pattern.compile(REX_NOCARD, Pattern.CASE_INSENSITIVE);
+
+    public static final String REGRP_SET = "setcode";
+    public static final String REGRP_COLLNR = "collnr";
+    public static final String REGRP_CARD = "cardname";
+    public static final String REGRP_CARDNO = "count";
+
+    public static final String REX_CARD_NAME = String.format("(?<%s>[a-zA-Z0-9',\\.:!\\+\\\"\\/\\-\\s]+)", REGRP_CARD);
+    public static final String REX_SET_CODE = String.format("(?<%s>[a-zA-Z0-9_]{2,7})", REGRP_SET);
+    public static final String REX_COLL_NUMBER = String.format("(?<%s>\\S?[0-9A-Z]+\\S?[A-Z]*)", REGRP_COLLNR);
+    public static final String REX_CARD_COUNT = String.format("(?<%s>[\\d]{1,2})(?<mult>x)?", REGRP_CARDNO);
+
+    // 1. Card-Set Request (Amount?, CardName, Set)
+    public static final String REX_CARD_SET_REQUEST = String.format(
+            "(%s\\s)?\\s*%s\\s*(\\s|\\||\\(|\\[|\\{)%s(\\s|\\)|\\]|\\})?",
+            REX_CARD_COUNT, REX_CARD_NAME, REX_SET_CODE);
+    public static final Pattern CARD_SET_PATTERN = Pattern.compile(REX_CARD_SET_REQUEST);
+
+    // 2. Set-Card Request (Amount?, Set, CardName)
+    public static final String REX_SET_CARD_REQUEST = String.format(
+            "(%s\\s)?\\s*(\\(|\\[|\\{)?%s(\\s+|\\)|\\]|\\}|\\|)\\s*%s\\s*",
+            REX_CARD_COUNT, REX_SET_CODE, REX_CARD_NAME);
+    public static final Pattern SET_CARD_PATTERN = Pattern.compile(REX_SET_CARD_REQUEST);
+
+    // 3. Full-Request (Amount?, CardName, Set, Collector Number|Art Index) - MTGArena Format
+    public static final String REX_FULL_REQUEST_CARD_SET = String.format(
+            "(%s\\s)?\\s*%s\\s*(\\||\\(|\\[|\\{|\\s)%s(\\s|\\)|\\]|\\})?\\s+%s",
+            REX_CARD_COUNT, REX_CARD_NAME, REX_SET_CODE, REX_COLL_NUMBER);
+    public static final Pattern CARD_SET_COLLNO_PATTERN = Pattern.compile(REX_FULL_REQUEST_CARD_SET);
+
+    // 4. Full-Request (Amount?, Set, CardName, Collector Number|Art Index) - Alternative for flexibility
+    public static final String REX_FULL_REQUEST_SET_CARD = String.format(
+            "^(%s\\s)?\\s*(\\(|\\[|\\{)?%s(\\s+|\\)|\\]|\\}|\\|)\\s*%s\\s+%s$",
+            REX_CARD_COUNT, REX_SET_CODE, REX_CARD_NAME, REX_COLL_NUMBER);
+    public static final Pattern SET_CARD_COLLNO_PATTERN = Pattern.compile(REX_FULL_REQUEST_SET_CARD);
+
+    // 5. Card-Only Request (Amount?)
+    public static final String REX_CARDONLY = String.format(
+            "(%s\\s)?\\s*%s", REX_CARD_COUNT, REX_CARD_NAME);
+    public static final Pattern CARD_ONLY_PATTERN = Pattern.compile(REX_CARDONLY);
 
-    private static final String CARD_REGEX_SET_NAME = String.format(
-            "(?<%s>[\\d]{1,2})?\\s*((\\(|\\[)?(?<%s>[a-zA-Z0-9]{3})(\\)|\\]|\\|)?)?" +
-                    "(?<%s>[a-zA-Z0-9',\\/\\-\\s]+)\\s*((\\|)(?<%s>[0-9A-Z]+\\S?[A-Z]*))?",
-                    REGRP_CARDNO, REGRP_SET, REGRP_CARD, REGRP_COLLNO);
-    private static final String CARD_REGEX_NAME_SET =
-            String.format("(?<%s>[\\d]{1,2})?\\s*(?<%s>[a-zA-Z0-9',\\/\\-\\s]+)\\s*((\\(|\\[|\\|)" +
-                    "(?<%s>[a-zA-Z0-9]{3})(\\)|\\])?)?\\s*(?<%s>[0-9A-Z]+\\S?[A-Z]*)?",
-                    REGRP_CARDNO, REGRP_CARD, REGRP_SET, REGRP_COLLNO);
 
-    private static final Pattern CARD_SET_NAME_PATTERN = Pattern.compile(CARD_REGEX_SET_NAME);
-    private static final Pattern CARD_NAME_SET_PATTERN = Pattern.compile(CARD_REGEX_NAME_SET);
     // CoreTypes (to recognise Tokens of type CardType
-    private static List<String> cardTypes = null;
-    // Note: Planes section is not included, as it is checked against "Plainswalker" (see `isDeckSectionName` method)
-    private static final CharSequence[] DECK_SECTION_NAMES = {"avatar", "commander", "schemes", "conspiracy",
-            "main", "card",
-            "side", "sideboard"};
+    private static CharSequence[] CARD_TYPES = allCardTypes();
 
-    private CardDb db;
-    private CardDb altDb;
-    private Date recognizeCardsPrintedBefore = null;
+    private static final CharSequence[] DECK_SECTION_NAMES = {"avatar", "commander",
+            "schemes", "conspiracy", "planes",
+            "main", "card", "mainboard", "side", "sideboard"};
+
+    private final CardDb db;
+    private final CardDb altDb;
+    private Date releaseDateConstraint = null;
     // This two parameters are controlled only via setter methods
     private List<String> allowedSetCodes = null;  // as imposed by current format
     private DeckFormat deckFormat = null;  //
@@ -185,34 +223,14 @@ public class DeckRecognizer {
     public DeckRecognizer(CardDb db, CardDb altDb) {
         this.db = db;
         this.altDb = altDb;
-
-        if (cardTypes == null) {
-            // CoreTypesNames
-            List<CardType.CoreType> coreTypes = Lists.newArrayList(CardType.CoreType.values());
-            cardTypes = new ArrayList<String>();
-            coreTypes.forEach(new Consumer<CardType.CoreType>() {
-                @Override
-                public void accept(CardType.CoreType ctype) {
-                    cardTypes.add(ctype.name().toLowerCase());
-                }
-            });
-            // Manual Additions:
-            // NOTE: "sorceries" is also included as it can be found in exported deck, even if it's incorrect.
-            // Example: https://deckstats.net/decks/70852/556925-artifacts/en - see Issue 1010
-            cardTypes.add("sorceries");  // Sorcery is the only name with different plural form
-            cardTypes.add("aura");  // in case.
-            cardTypes.add("mana");  // "Mana" (see Issue 1010)
-            cardTypes.add("spell");
-            cardTypes.add("other spell");
-        }
     }
 
     public Token recognizeLine(final String rawLine) {
         if (StringUtils.isBlank(rawLine.trim()))
             return null;
-        if (StringUtils.startsWith(rawLine.trim(), LINE_COMMENT_DELIMITER)) {
-            return new Token(TokenType.Comment, 0, rawLine);
-        }
+        if (StringUtils.startsWith(rawLine.trim(), LINE_COMMENT_DELIMITER))
+            return new Token(TokenType.COMMENT, 0, rawLine);
+
         final char smartQuote = (char) 8217;
         String line = rawLine.trim().replace(smartQuote, '\'');
 
@@ -230,46 +248,63 @@ public class DeckRecognizer {
 
         Token result = recogniseCardToken(line);
         if (result == null)
-            result = recogniseNonCardToken(line, 1);
-        return result != null ? result : new Token(TokenType.UnknownText, 0, line);
+            result = recogniseNonCardToken(line);
+        return result != null ? result : new Token(TokenType.UNKNOWN_TEXT, 0, line);
     }
 
-    private Token recogniseCardToken(final String text) {
+    public Token recogniseCardToken(final String text) {
         String line = text.trim();
+        Token uknonwnCardToken = null;
+
         // TODO: recognize format: http://topdeck.ru/forum/index.php?showtopic=12711
         // @leriomaggio: DONE!
-        List<Matcher> cardMatchers = getRegexMatchers(line);
-        for (Matcher cardMatcher : cardMatchers) {
-            String cardName = cardMatcher.group(REGRP_CARD);
-            String ccount = cardMatcher.group(REGRP_CARDNO);
-            String setCode = cardMatcher.group(REGRP_SET);
-            String collNo = cardMatcher.group(REGRP_COLLNO);
-            int cardCount = ccount != null ? Integer.parseInt(ccount) : 1;
-            // if any, it will be tried to convert specific collector number to art index (useful for lands).
-            String collectorNumber = collNo != null ? collNo : IPaperCard.NO_COLLECTOR_NUMBER;
-
+        List<Matcher> cardMatchers = getRegExMatchers(line);
+        for (Matcher matcher : cardMatchers) {
+            String cardName = getRexGroup(matcher, REGRP_CARD);
+            if (cardName == null)
+                continue;
+            cardName = cardName.trim();
             //Avoid hit the DB - check whether cardName is contained in the DB
             CardDb.CardRequest cr = CardDb.CardRequest.fromString(cardName.trim());  // to account for any FOIL request
             if (!foundInCardDb(cr.cardName))
                 continue;  // skip to the next matcher!
-            // Ok Now we're sure the cardName is correct. Now check for setCode
-            CardEdition edition = StaticData.instance().getEditions().get(setCode);
-            if (edition != null){
+            String ccount = getRexGroup(matcher, REGRP_CARDNO);
+            String setCode = getRexGroup(matcher, REGRP_SET);
+            String collNo = getRexGroup(matcher, REGRP_COLLNR);
+            int cardCount = ccount != null ? Integer.parseInt(ccount) : 1;
+            // if any, it will be tried to convert specific collector number to art index (useful for lands).
+            String collectorNumber = collNo != null ? collNo : IPaperCard.NO_COLLECTOR_NUMBER;
+            int artIndex;
+            try {
+                artIndex = Integer.parseInt(collectorNumber);
+            } catch (NumberFormatException ex){
+                artIndex = IPaperCard.NO_ART_INDEX;
+            }
+
+            if (setCode != null) {
+                // Ok Now we're sure the cardName is correct. Now check for setCode
+                CardEdition edition = StaticData.instance().getEditions().get(setCode);
+                if (edition == null) {
+                    // set the case for unknown card (in case) and continue to the next for any better matching
+                    uknonwnCardToken = Token.UnknownCard(cardName, setCode, cardCount);
+                    continue;
+                }
+                if (isNotCompliantWithReleaseDateRestrictions(edition))
+                    return Token.InvalidCard(cr.cardName, edition.getCode(), cardCount);
+
                 // we now name is ok, set is ok - we just need to be sure about collector number (if any)
                 // and if that card can be actually found in the requested set.
                 // IOW: we should account for wrong request, e.g. Counterspell|FEM - just doesn't exist!
-                PaperCard pc = getCardFromSet(cr.cardName, edition, collectorNumber, cr.isFoil);
+                PaperCard pc = this.getCardFromSet(cr.cardName, edition, collectorNumber, artIndex, cr.isFoil);
                 if (pc != null) {
                     // ok so the card has been found - let's see if there's any restriction on the set
-                    if (this.allowedSetCodes != null && !this.allowedSetCodes.contains(setCode))
+                    if (isIllegalSetInGameFormat(setCode) || isIllegalCardInDeckFormat(pc))
                         // Mark as illegal card
-                        return Token.illegalCard(pc.getName(), pc.getEdition(), cardCount);
-                    if (this.deckFormat != null && !deckFormat.isLegalCard(pc))
-                        return Token.illegalCard(pc.getName(), pc.getEdition(), cardCount);
-                    return Token.knownCard(pc, cardCount);
+                        return Token.IllegalCard(pc.getName(), pc.getEdition(), cardCount);
+                    return Token.KnownCard(pc, cardCount);
                 }
                 // UNKNOWN card as in the Counterspell|FEM case
-                return Token.unknownCard(cardName, setCode, cardCount);
+                return Token.UnknownCard(cardName, setCode, cardCount);
             }
             // ok so we can simply ignore everything but card name - as set code does not exist
             // At this stage, we know the card name exists in the DB so a Card MUST be found
@@ -277,43 +312,89 @@ public class DeckRecognizer {
             // In that case, an illegalCard token will be returned!
             PaperCard pc = this.getCardFromSupportedEditions(cr.cardName, cr.isFoil);
             if (pc != null){
-                if (this.deckFormat != null && !deckFormat.isLegalCard(pc))
-                    return Token.illegalCard(pc.getName(), pc.getEdition(), cardCount);
-                return Token.knownCard(pc, cardCount);
+                if (isIllegalCardInDeckFormat(pc))
+                    return Token.IllegalCard(pc.getName(), pc.getEdition(), cardCount);
+                return Token.KnownCard(pc, cardCount);
             }
-            return Token.illegalCard(cardName, "", cardCount);
+            return Token.IllegalCard(cardName, "", cardCount);
         }
-        return null;
+        return uknonwnCardToken;  // either null or unknown card
+    }
+
+    private String getRexGroup(Matcher matcher, String groupName){
+        String rexGroup;
+        try{
+            rexGroup = matcher.group(groupName);
+        } catch (IllegalArgumentException ex) {
+            rexGroup = null;
+        }
+        return rexGroup;
+    }
+
+    private boolean isIllegalCardInDeckFormat(PaperCard pc) {
+        return this.deckFormat != null && !deckFormat.isLegalCard(pc);
+    }
+
+    private boolean isIllegalSetInGameFormat(String setCode) {
+        return this.allowedSetCodes != null && !this.allowedSetCodes.contains(setCode);
+    }
+
+    private boolean isNotCompliantWithReleaseDateRestrictions(CardEdition edition){
+        return this.releaseDateConstraint != null && edition.getDate().compareTo(this.releaseDateConstraint) >= 0;
     }
 
     private boolean foundInCardDb(String cardName){
-        return (this.db.contains(cardName.trim()) || this.altDb.contains(cardName.trim()));
+        return this.db.contains(cardName) || this.altDb.contains(cardName);
     }
 
-    private List<Matcher> getRegexMatchers(String line){
-        final Matcher setBeforeNameMatcher = CARD_SET_NAME_PATTERN.matcher(line);
-        final Matcher setAfterNameMatcher = CARD_NAME_SET_PATTERN.matcher(line);
+    private List<Matcher> getRegExMatchers(String line) {
         List<Matcher> matchers = new ArrayList<>();
-        if (setBeforeNameMatcher.find())
-            matchers.add(setAfterNameMatcher);
-        if (setAfterNameMatcher.find())
-            matchers.add(setBeforeNameMatcher);
+        Pattern[] patternsWithCollNumber = new Pattern[] {
+                CARD_SET_COLLNO_PATTERN,
+                SET_CARD_COLLNO_PATTERN
+        };
+        for (Pattern pattern : patternsWithCollNumber) {
+            Matcher matcher = pattern.matcher(line);
+            if (matcher.matches() && getRexGroup(matcher, REGRP_SET) != null &&
+                    getRexGroup(matcher, REGRP_COLLNR) != null)
+                matchers.add(matcher);
+        }
+        Pattern[] OtherPatterns = new Pattern[] {  // Order counts
+                CARD_SET_PATTERN,
+                SET_CARD_PATTERN,
+                CARD_ONLY_PATTERN
+        };
+        for (Pattern pattern : OtherPatterns) {
+            Matcher matcher = pattern.matcher(line);
+            if (matcher.matches())
+                matchers.add(matcher);
+        }
         return matchers;
-
-//        if (fullMatchSetAfter.length() == 0 && fullMatchSetBefore.length() == 0)
-//            return null;
-//        if (fullMatchSetBefore.equals(line))
-//            return setBeforeNameMatcher;
-//        if (fullMatchSetAfter.equals(line))
-//            return setAfterNameMatcher;
-//        return fullMatchSetAfter.length() > fullMatchSetBefore.length() ? setAfterNameMatcher : setBeforeNameMatcher;
     }
 
     private PaperCard getCardFromSet(final String cardName, final CardEdition edition,
-                                     final String collectorNumber, final boolean isFoil) {
+                                     final String collectorNumber, final int artIndex,
+                                     final boolean isFoil) {
+        // Try with collector number first
         PaperCard result = this.db.getCardFromSet(cardName, edition, collectorNumber, isFoil);
         if (result == null)
             result = this.altDb.getCardFromSet(cardName, edition, collectorNumber, isFoil);
+        if (result == null && !collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER) &&
+                artIndex != IPaperCard.NO_ART_INDEX){
+            // So here we know cardName exists (checked before invoking this method)
+            // and also a Collector Number was specified.
+            // The only case we would reach this point is either due to a wrong edition-card match
+            // (later resulting in Unknown card - e.g. "Counterspell|FEM") or due to the fact that
+            // art Index was specified instead of collector number! Let's give it a go with that
+            // but only if artIndex is not NO_ART_INDEX (e.g. collectorNumber = "*32")
+            int maxArtForCard = this.db.contains(cardName) ? this.db.getMaxArtIndex(cardName) :
+                    this.altDb.getMaxArtIndex(cardName);
+            if (artIndex <= maxArtForCard){
+                // if collNr was "78", it's hardly an artIndex. It was just the wrong collNr for the requested card
+                result = this.db.contains(cardName) ? this.db.getCardFromSet(cardName, edition, artIndex, isFoil) :
+                        this.altDb.getCardFromSet(cardName, edition, artIndex, isFoil);
+            }
+        }
         return result;
     }
 
@@ -322,139 +403,109 @@ public class DeckRecognizer {
         if (this.allowedSetCodes != null && this.allowedSetCodes.size() > 0)
             filter = (Predicate<PaperCard>) this.db.isLegal(this.allowedSetCodes);
         String reqInfo = CardDb.CardRequest.compose(cardName, isFoil);
-        PaperCard result = this.db.getCardFromEditions(reqInfo, filter);
-        if (result == null)
-            result = this.altDb.getCardFromEditions(reqInfo, filter);
+        PaperCard result;
+        if (this.releaseDateConstraint != null){
+            result = this.db.getCardFromEditionsReleasedBefore(reqInfo,
+                    this.releaseDateConstraint, filter);
+            if (result == null)
+                result = this.altDb.getCardFromEditionsReleasedBefore(reqInfo,
+                        this.releaseDateConstraint, filter);
+        }
+        else {
+            result = this.db.getCardFromEditions(reqInfo, filter);
+            if (result == null)
+                result = this.altDb.getCardFromEditions(reqInfo, filter);
+        }
         return result;
     }
 
-    private static Token recogniseNonCardToken(final String text, final int n) {
+    public Token recogniseNonCardToken(final String text) {
         if (isDeckSectionName(text)) {
-            return Token.deckSection(text.toLowerCase().trim());
+            String tokenText = getNonCardTokenText(text.toLowerCase().trim());
+            return Token.DeckSection(tokenText);
         }
         if (isCardType(text)) {
-            return new Token(TokenType.CardType, n, text);
+            String tokenText = getNonCardTokenText(text);
+            return new Token(TokenType.CARD_TYPE, tokenText);
         }
         if (isDeckName(text)) {
             String deckName = getDeckName(text);
-            return new Token(TokenType.DeckName, n, deckName);
+            return new Token(TokenType.DECK_NAME, deckName);
         }
         return null;
     }
 
+    private static String getNonCardTokenText(final String line){
+        Matcher noncardMatcher = NONCARD_PATTERN.matcher(line);
+        if (!noncardMatcher.matches())
+            return "";
+        return noncardMatcher.group(REGRP_NOCARD);
+    }
+
+    private static CharSequence[] allCardTypes(){
+        List<String> cardTypesList = new ArrayList<>();
+        // CoreTypesNames
+        List<CardType.CoreType> coreTypes = Lists.newArrayList(CardType.CoreType.values());
+        for (CardType.CoreType coreType : coreTypes)
+            cardTypesList.add(coreType.name().toLowerCase());
+        // Manual Additions:
+        // NOTE: "sorceries" is also included as it can be found in exported deck, even if it's incorrect.
+        // Example: https://deckstats.net/decks/70852/556925-artifacts/en - see Issue 1010
+        cardTypesList.add("sorceries");  // Sorcery is the only name with different plural form
+        cardTypesList.add("aura");  // in case.
+        cardTypesList.add("mana");  // "Mana" (see Issue 1010)
+        cardTypesList.add("spell");
+        cardTypesList.add("other spell");
+        cardTypesList.add("planeswalker");
+        return cardTypesList.toArray(new CharSequence[cardTypesList.size()]);
+    }
+
     // NOTE: Card types recognition is ONLY used for style formatting in the Import Editor
     // This won't affect the import process of cards in any way !-)
-    private static boolean isCardType(final String lineAsIs) {
-        final String line = lineAsIs.toLowerCase().trim();
-        for (final String cardType : cardTypes) {
-            if (line.startsWith(cardType))
-                return true;
-        }
-        return false;
+    public static boolean isCardType(final String lineAsIs) {
+        String line = lineAsIs.toLowerCase().trim();
+        Matcher noncardMatcher = NONCARD_PATTERN.matcher(line);
+        if (!noncardMatcher.matches())
+            return false;
+        String nonCardToken = noncardMatcher.group(REGRP_NOCARD);
+        return StringUtils.containsAny(nonCardToken, CARD_TYPES);
     }
 
-    private static boolean isDeckName(final String lineAsIs) {
+    public static boolean isDeckName(final String lineAsIs) {
         final String line = lineAsIs.trim();
         final Matcher deckNameMatcher = DECK_NAME_PATTERN.matcher(line);
-        return (deckNameMatcher.find());
+        boolean matches = deckNameMatcher.matches();
+        return matches;
     }
 
-    private static String getDeckName(final String text) {
+    public static String getDeckName(final String text) {
         String line = text.trim();
         final Matcher deckNamePattern = DECK_NAME_PATTERN.matcher(line);
-        if (deckNamePattern.find())
+        if (deckNamePattern.matches())
             return deckNamePattern.group(REGRP_DECKNAME);  // Deck name is at match 7
-        return line;
+        return "";
     }
 
-    private static boolean isDeckSectionName(final String text) {
+    public static boolean isDeckSectionName(final String text) {
         String line = text.toLowerCase().trim();
-        if (StringUtils.containsAny(line, DECK_SECTION_NAMES))
-            return true;
-        return line.contains("planes") && !line.contains("planeswalker");
+        Matcher noncardMatcher = NONCARD_PATTERN.matcher(line);
+        if (!noncardMatcher.matches())
+            return false;
+        String nonCardToken = noncardMatcher.group(REGRP_NOCARD);
+        return StringUtils.equalsAnyIgnoreCase(nonCardToken, DECK_SECTION_NAMES);
     }
 
-    public void setDateConstraint(int month, Integer year) {
+    public void setDateConstraint(int year, int month) {
         Calendar ca = Calendar.getInstance();
         ca.set(year, month, 1);
-        recognizeCardsPrintedBefore = ca.getTime();
+        releaseDateConstraint = ca.getTime();
     }
 
-    public void setCardEditionConstrain(List<String> allowedSetCodes){
+    public void setGameFormatConstraint(List<String> allowedSetCodes){
         this.allowedSetCodes = allowedSetCodes;
     }
 
     public void setDeckFormatConstraint(DeckFormat deckFormat0){
         this.deckFormat = deckFormat0;
     }
-    
-//    private Token recognizePossibleNameAndNumber(final String name, final int n) {
-//        PaperCard pc = tryGetCard(name);
-//        if (null != pc) {
-//            return Token.knownCard(pc, n);
-//        }
-//
-//        // TODO: recognize format: http://topdeck.ru/forum/index.php?showtopic=12711
-//        //final Matcher foundEditionName = READ_SEPARATED_EDITION.matcher(name);
-//
-//        final Token known = DeckRecognizer.recognizeNonCard(name, n);
-//        return null == known ? Token.unknownCard(name, n) : known;
-//    }
-//
-//    private static Token recognizeNonCard(final String text, final int n) {
-//        if (DeckRecognizer.isDecoration(text)) {
-//            return new Token(TokenType.Comment, n, text);
-//        }
-//        if (DeckRecognizer.isSectionName(text)) {
-//            return new Token(TokenType.SectionName, n, text);
-//        }
-//        return null;
-//    }
-
-//    private static final String[] KNOWN_COMMENTS = new String[] { "land", "lands", "creatures", "creature", "spells",
-//            "enchantments", "other spells", "artifacts" };
-//    private static final String[] KNOWN_COMMENT_PARTS = new String[] { "card" };
-//
-//    private static boolean isDecoration(final String lineAsIs) {
-//        final String line = lineAsIs.toLowerCase();
-//        for (final String s : DeckRecognizer.KNOWN_COMMENT_PARTS) {
-//            if (line.contains(s)) {
-//                return true;
-//            }
-//        }
-//        for (final String s : DeckRecognizer.KNOWN_COMMENTS) {
-//            if (line.equalsIgnoreCase(s)) {
-//                return true;
-//            }
-//        }
-//        return false;
-//    }
-//
-//    private static boolean isSectionName(final String line) {
-//        if (line.toLowerCase().contains("side")) {
-//            return true;
-//        }
-//        if (line.toLowerCase().contains("main")) {
-//            return true;
-//        }
-//        if (line.toLowerCase().contains("commander")) {
-//            return true;
-//        }
-//        if (line.toLowerCase().contains("planes")) {
-//            return true;
-//        }
-//        if (line.toLowerCase().contains("schemes")) {
-//            return true;
-//        }
-//        if (line.toLowerCase().contains("vanguard")) {
-//            return true;
-//        }
-//        return false;
-//    }
-//
-//    public void setDateConstraint(int month, Integer year) {
-//        Calendar ca = Calendar.getInstance();
-//        ca.set(year, month, 1);
-//        recognizeCardsPrintedBefore = ca.getTime();
-//    }
 }
diff --git a/forge-gui-desktop/src/test/java/forge/deck/DeckRecognizerTest.java b/forge-gui-desktop/src/test/java/forge/deck/DeckRecognizerTest.java
new file mode 100644
index 00000000000..d163a09528f
--- /dev/null
+++ b/forge-gui-desktop/src/test/java/forge/deck/DeckRecognizerTest.java
@@ -0,0 +1,1160 @@
+package forge.deck;
+
+import forge.StaticData;
+import forge.card.CardDb;
+import forge.card.CardEdition;
+import forge.card.ForgeCardMockTestCase;
+import forge.item.IPaperCard;
+import forge.item.PaperCard;
+import forge.deck.DeckRecognizer.Token;
+import forge.deck.DeckRecognizer.TokenType;
+import forge.model.FModel;
+import org.testng.annotations.Test;
+import static org.testng.Assert.*;
+
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class DeckRecognizerTest extends ForgeCardMockTestCase {
+
+    private Set<String> mtgUniqueCardNames;
+    private Set<String> mtgUniqueSetCodes;
+    private Set<String> mtgUniqueCollectorNumbers;
+
+    private void initMaps(){
+        StaticData magicDb = FModel.getMagicDb();
+        mtgUniqueCardNames = new HashSet<>();
+        mtgUniqueSetCodes = new HashSet<>();
+        mtgUniqueCollectorNumbers = new HashSet<>();
+        Collection<PaperCard> fullCardDb = magicDb.getCommonCards().getAllCards();
+        for (PaperCard card : fullCardDb) {
+            this.mtgUniqueCardNames.add(card.getName());
+            CardEdition e = magicDb.getCardEdition(card.getEdition());
+            if (e != null) {
+                this.mtgUniqueSetCodes.add(e.getCode());
+                this.mtgUniqueSetCodes.add(e.getScryfallCode());
+            }
+            String cn = card.getCollectorNumber();
+            if (!cn.equals(IPaperCard.NO_COLLECTOR_NUMBER))
+                this.mtgUniqueCollectorNumbers.add(cn);
+        }
+    }
+
+    /* ======================================
+     * TEST SINGLE (Card and Non-Card) Parts
+     * - CardName
+     * - Card Amount
+     * - Set Code
+     * - Collector Number
+     * - Deck Name
+     * - Card Type
+     * - Deck Section
+     * ======================================
+     */
+
+    @Test void testMatchingCardName(){
+        if (mtgUniqueCardNames == null)
+            this.initMaps();
+        Pattern cardNamePattern = Pattern.compile(DeckRecognizer.REX_CARD_NAME);
+        for (String cardName : this.mtgUniqueCardNames){
+            Matcher cardNameMatcher = cardNamePattern.matcher(cardName);
+            assertTrue(cardNameMatcher.matches(), "Fail on " + cardName);
+            String matchedCardName = cardNameMatcher.group(DeckRecognizer.REGRP_CARD);
+            assertEquals(matchedCardName, cardName, "Fail on " + cardName);
+        }
+    }
+
+    @Test void testMatchingFoilCardName(){
+        String foilCardName = "Counter spell+";
+        Pattern cardNamePattern = Pattern.compile(DeckRecognizer.REX_CARD_NAME);
+        Matcher cardNameMatcher = cardNamePattern.matcher(foilCardName);
+        assertTrue(cardNameMatcher.matches(), "Fail on " + foilCardName);
+        String matchedCardName = cardNameMatcher.group(DeckRecognizer.REGRP_CARD);
+        assertEquals(matchedCardName, foilCardName, "Fail on " + foilCardName);
+    }
+
+
+    @Test void testMatchingSetCodes(){
+        if (mtgUniqueCardNames == null)
+            this.initMaps();
+        Pattern setCodePattern = Pattern.compile(DeckRecognizer.REX_SET_CODE);
+        for (String setCode : this.mtgUniqueSetCodes){
+            Matcher setCodeMatcher = setCodePattern.matcher(setCode);
+            assertTrue(setCodeMatcher.matches(), "Fail on " + setCode);
+            String matchedSetCode = setCodeMatcher.group(DeckRecognizer.REGRP_SET);
+            assertEquals(matchedSetCode, setCode, "Fail on " + setCode);
+        }
+    }
+
+    @Test void testMatchingCollectorNumber(){
+        if (mtgUniqueCardNames == null)
+            this.initMaps();
+        Pattern collNumberPattern = Pattern.compile(DeckRecognizer.REX_COLL_NUMBER);
+        for (String collectorNumber : this.mtgUniqueCollectorNumbers){
+            Matcher collNumberMatcher = collNumberPattern.matcher(collectorNumber);
+            assertTrue(collNumberMatcher.matches());
+            String matchedCollNr = collNumberMatcher.group(DeckRecognizer.REGRP_COLLNR);
+            assertEquals(matchedCollNr, collectorNumber, "Fail on " + collectorNumber);
+        }
+    }
+
+    @Test void testCardQuantityRequest(){
+        Pattern cardCountPattern = Pattern.compile(DeckRecognizer.REX_CARD_COUNT);
+        String[] correctAmountRequests = new String[] {"0", "2", "12", "4", "8x", "12x"};
+        String[] inCorrectAmountRequests = new String[] {"-2", "-23", "NO", "133"};
+
+        for (String correctReq : correctAmountRequests) {
+            Matcher matcher = cardCountPattern.matcher(correctReq);
+            assertTrue(matcher.matches());
+            String expectedRequestAmount = correctReq;
+            if (correctReq.endsWith("x"))
+                expectedRequestAmount = correctReq.substring(0, correctReq.length()-1);
+
+            assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), expectedRequestAmount);
+            int expectedAmount = Integer.parseInt(expectedRequestAmount);
+            assertEquals(Integer.parseInt(matcher.group(DeckRecognizer.REGRP_CARDNO)), expectedAmount);
+        }
+
+        for (String incorrectReq : inCorrectAmountRequests) {
+            Matcher matcher = cardCountPattern.matcher(incorrectReq);
+            assertFalse(matcher.matches());
+        }
+    }
+
+    @Test void testMatchDeckName(){
+        Pattern deckNamePattern = DeckRecognizer.DECK_NAME_PATTERN;
+
+        String matchingDeckName = "Deck Name: Red Green Aggro";
+        Matcher deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
+        assertTrue(deckNameMatcher.matches());
+        assertTrue(DeckRecognizer.isDeckName(matchingDeckName));
+        assertEquals(deckNameMatcher.group(DeckRecognizer.REGRP_DECKNAME), "Red Green Aggro");
+        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "Red Green Aggro");
+
+        matchingDeckName = "Name: Red Green Aggro";
+        deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
+        assertTrue(deckNameMatcher.matches());
+        assertTrue(DeckRecognizer.isDeckName(matchingDeckName));
+        assertEquals(deckNameMatcher.group(DeckRecognizer.REGRP_DECKNAME), "Red Green Aggro");
+        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "Red Green Aggro");
+
+        matchingDeckName = "Name:Red Green Aggro";
+        deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
+        assertTrue(deckNameMatcher.matches());
+        assertTrue(DeckRecognizer.isDeckName(matchingDeckName));
+        assertEquals(deckNameMatcher.group(DeckRecognizer.REGRP_DECKNAME), "Red Green Aggro");
+        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "Red Green Aggro");
+
+        matchingDeckName = "Name:       Red Green Aggro";
+        deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
+        assertTrue(deckNameMatcher.matches());
+        assertTrue(DeckRecognizer.isDeckName(matchingDeckName));
+        assertEquals(deckNameMatcher.group(DeckRecognizer.REGRP_DECKNAME), "Red Green Aggro");
+        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "Red Green Aggro");
+
+        matchingDeckName = "Deck:Red Green Aggro";
+        deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
+        assertTrue(deckNameMatcher.matches());
+        assertTrue(DeckRecognizer.isDeckName(matchingDeckName));
+        assertEquals(deckNameMatcher.group(DeckRecognizer.REGRP_DECKNAME), "Red Green Aggro");
+        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "Red Green Aggro");
+
+        // Case Insensitive
+        matchingDeckName = "deck name: Red Green Aggro";
+        deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
+        assertTrue(deckNameMatcher.matches());
+        assertTrue(DeckRecognizer.isDeckName(matchingDeckName));
+        assertEquals(deckNameMatcher.group(DeckRecognizer.REGRP_DECKNAME), "Red Green Aggro");
+        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "Red Green Aggro");
+
+        // Failing Cases
+        matchingDeckName = ":Red Green Aggro";
+        deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
+        assertFalse(deckNameMatcher.matches());
+        assertFalse(DeckRecognizer.isDeckName(matchingDeckName));
+        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "");
+
+        matchingDeckName = "Red Green Aggro";
+        deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
+        assertFalse(deckNameMatcher.matches());
+        assertFalse(DeckRecognizer.isDeckName(matchingDeckName));
+        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "");
+
+        matchingDeckName = "Name-Red Green Aggro";
+        deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
+        assertFalse(deckNameMatcher.matches());
+        assertFalse(DeckRecognizer.isDeckName(matchingDeckName));
+        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "");
+
+        matchingDeckName = "Deck.Red Green Aggro";
+        deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
+        assertFalse(deckNameMatcher.matches());
+        assertFalse(DeckRecognizer.isDeckName(matchingDeckName));
+        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "");
+
+        matchingDeckName = ":Red Green Aggro";
+        deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
+        assertFalse(deckNameMatcher.matches());
+        assertFalse(DeckRecognizer.isDeckName(matchingDeckName));
+        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "");
+    }
+
+    @Test void testMatchDeckSectionNames(){
+        String[] dckSections = new String[] {"Main", "main", "Mainboard",
+                "Sideboard", "Side", "Schemes", "Avatar", "avatar", "Commander", "Conspiracy", "card"};
+        for (String section : dckSections)
+            assertTrue(DeckRecognizer.isDeckSectionName(section), "Unrecognised Deck Section: " + section);
+
+        assertFalse(DeckRecognizer.isDeckSectionName("Avatar of Hope"));
+        assertFalse(DeckRecognizer.isDeckSectionName("Conspiracy Theory"));
+        assertFalse(DeckRecognizer.isDeckSectionName("Planeswalker's Mischief"));
+        assertFalse(DeckRecognizer.isDeckSectionName("Demon of Dark Schemes"));
+
+        String[] deckSectionEntriesFoundInMDExportFromTappedOut = new String[]{
+                "## Sideboard (15)", "## Mainboard (86)"};
+        for (String entry: deckSectionEntriesFoundInMDExportFromTappedOut)
+            assertTrue(DeckRecognizer.isDeckSectionName(entry), "Fail on "+entry);
+
+        String[] deckSectionEntriesFoundInDCKFormat = new String[] {"[Main]", "[Sideboard]"};
+        for (String entry: deckSectionEntriesFoundInDCKFormat)
+            assertTrue(DeckRecognizer.isDeckSectionName(entry), "Fail on "+entry);
+    }
+
+    @Test void testMatchCardTypes(){
+        String[] cardTypes = new String[] {"Spell", "instants", "Sorceries", "Sorcery",
+                "Artifact", "creatures", "land"};
+        for (String cardType : cardTypes)
+            assertTrue(DeckRecognizer.isCardType(cardType), "Fail on "+cardType);
+
+        String[] cardTypesEntriesFoundInMDExportFromTappedOut = new String[]{
+                "### Instant (14)", "### Sorcery (9)", "### Artifact (14)",
+                "### Creature (2)", "### Land (21)", };
+        for (String entry: cardTypesEntriesFoundInMDExportFromTappedOut)
+            assertTrue(DeckRecognizer.isCardType(entry), "Fail on "+entry);
+
+        String[] cardTypesInDecFormat = new String[] {
+                "//Lands", "//Artifacts", "//Enchantments",
+                "//Instants", "//Sorceries", "//Planeswalkers",
+                "//Creatures"};
+        for (String entry : cardTypesInDecFormat)
+            assertTrue(DeckRecognizer.isCardType(entry), "Fail on " + entry);
+    }
+
+    /*=============================
+    * TEST RECOGNISE NON-CARD LINES
+    * =============================
+    */
+    @Test void testMatchNonCardLine(){
+        StaticData magicDb = FModel.getMagicDb();
+        CardDb db = magicDb.getCommonCards();
+        CardDb altDb = magicDb.getVariantCards();
+        DeckRecognizer recognizer = new DeckRecognizer(db, altDb);
+
+        // Test Token Types
+        Token t = recognizer.recogniseNonCardToken("//Lands");
+        assertNotNull(t);
+        assertEquals(t.getType(), TokenType.CARD_TYPE);
+        assertEquals(t.getText(), "Lands");
+        assertEquals(t.getNumber(), 0);
+
+        t = recognizer.recogniseNonCardToken("[Main]");
+        assertNotNull(t);
+        assertEquals(t.getType(), TokenType.DECK_SECTION_NAME);
+        assertEquals(t.getText(), "Main");
+        assertEquals(t.getNumber(), 0);
+
+        t = recognizer.recogniseNonCardToken("## Mainboard (75)");
+        assertNotNull(t);
+        assertEquals(t.getType(), TokenType.DECK_SECTION_NAME);
+        assertEquals(t.getText(), "Main");
+        assertEquals(t.getNumber(), 0);
+
+        t = recognizer.recogniseNonCardToken("### Artifact (3)");
+        assertNotNull(t);
+        assertEquals(t.getType(), TokenType.CARD_TYPE);
+        assertEquals(t.getText(), "Artifact");
+        assertEquals(t.getNumber(), 0);
+
+        t = recognizer.recogniseNonCardToken("Enchantments");
+        assertNotNull(t);
+        assertEquals(t.getType(), TokenType.CARD_TYPE);
+        assertEquals(t.getText(), "Enchantments");
+        assertEquals(t.getNumber(), 0);
+
+        t = recognizer.recogniseNonCardToken("//Name: Artifacts from DeckStats.net");
+        assertNotNull(t);
+        assertEquals(t.getType(), TokenType.DECK_NAME);
+        assertEquals(t.getText(), "Artifacts from DeckStats");
+        assertEquals(t.getNumber(), 0);
+
+        t = recognizer.recogniseNonCardToken("Name: OLDSCHOOL 93-94 Red Green Aggro by Zombies with JetPack");
+        assertNotNull(t);
+        assertEquals(t.getType(), TokenType.DECK_NAME);
+        assertEquals(t.getText(), "OLDSCHOOL 93-94 Red Green Aggro by Zombies with JetPack");
+        assertEquals(t.getNumber(), 0);
+    }
+
+    /*=============================
+     * TEST RECOGNISE CARD LINES
+     * =============================
+     */
+
+    // === Card-Set Request
+    @Test void testValidMatchCardSetLine(){
+        String validRequest = "1 Power Sink TMP";
+        Matcher matcher = DeckRecognizer.CARD_SET_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "1");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "Power Sink TMP";  // no count
+        matcher = DeckRecognizer.CARD_SET_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "Power Sink tmp";  // set cde in lower case
+        matcher = DeckRecognizer.CARD_SET_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "tmp");
+
+        validRequest = "10 Power Sink TMP";  // double digits
+        matcher = DeckRecognizer.CARD_SET_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "10");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        // x multiplier symbol in card count
+        validRequest = "12x Power Sink TMP";
+        matcher = DeckRecognizer.CARD_SET_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "12");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        // -- Set code with delimiters
+        validRequest = "Power Sink|TMP";  // pipe delimiter
+        matcher = DeckRecognizer.CARD_SET_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "Power Sink (TMP)";  // MTGArena-like
+        matcher = DeckRecognizer.CARD_SET_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink "); // requires trim
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "Power Sink|TMP";  // pipe delimiter
+        matcher = DeckRecognizer.CARD_SET_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "Power Sink [TMP]"; // .Dec-like delimiter
+        matcher = DeckRecognizer.CARD_SET_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink "); // TRIM
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "Power Sink|TMP";  // pipe delimiter
+        matcher = DeckRecognizer.CARD_SET_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "Power Sink {TMP}";  // Alternative braces delimiter
+        matcher = DeckRecognizer.CARD_SET_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink ");  // TRIM
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        // -- also valid = delimiters can also be partial
+
+        validRequest = "Power Sink (TMP";  // pipe delimiter
+        matcher = DeckRecognizer.CARD_SET_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink ");  // TRIM
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "Power Sink(TMP)";  // No space
+        matcher = DeckRecognizer.CARD_SET_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "Power Sink|TMP";  // pipe delimiter
+        matcher = DeckRecognizer.CARD_SET_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "Power Sink [TMP)";  // Mixed
+        matcher = DeckRecognizer.CARD_SET_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink ");  // TRIM
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "Power Sink TMP]";  // last bracket to be stripped, but using space from card name
+        matcher = DeckRecognizer.CARD_SET_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+    }
+
+    @Test void testInvalidMatchCardSetLine(){
+        // == Invalid Cases for this REGEX
+        // Remeber: this rex *always* expects a Set Code!
+
+        String invalidRequest = "Power Sink ";  // mind last space
+        Matcher matcher = DeckRecognizer.CARD_SET_PATTERN.matcher(invalidRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power");
+        assertNotNull(matcher.group(DeckRecognizer.REGRP_SET));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "Sink");
+
+        invalidRequest = "22 Power Sink";  // mind last space
+        matcher = DeckRecognizer.CARD_SET_PATTERN.matcher(invalidRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "22");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power");
+        assertNotNull(matcher.group(DeckRecognizer.REGRP_SET));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "Sink");
+    }
+
+    // === Set-Card Request
+    @Test void testValidMatchSetCardLine(){
+        String validRequest = "1 TMP Power Sink";
+        Matcher matcher = DeckRecognizer.SET_CARD_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "1");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "TMP Power Sink";  // no count
+        matcher = DeckRecognizer.SET_CARD_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "tmp Power Sink";  // set code in lowercase
+        matcher = DeckRecognizer.SET_CARD_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "tmp");
+
+        validRequest = "10 TMP Power Sink";  // double digits
+        matcher = DeckRecognizer.SET_CARD_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "10");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        // x multiplier symbol in card count
+        validRequest = "12x TMP Power Sink";
+        matcher = DeckRecognizer.SET_CARD_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "12");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        // -- separators
+        validRequest = "TMP|Power Sink";  // pipe-after set code
+        matcher = DeckRecognizer.SET_CARD_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "(TMP)Power Sink";  // parenthesis
+        matcher = DeckRecognizer.SET_CARD_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "[TMP]Power Sink";  // brackets
+        matcher = DeckRecognizer.SET_CARD_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "{TMP}Power Sink";  // braces
+        matcher = DeckRecognizer.SET_CARD_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "TMP    Power Sink";  // lots of spaces
+        matcher = DeckRecognizer.SET_CARD_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "(TMP|Power Sink";  // mixed
+        matcher = DeckRecognizer.SET_CARD_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "(TMP]Power Sink";  // mixed 2
+        matcher = DeckRecognizer.SET_CARD_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "TMP]Power Sink";  // partial
+        matcher = DeckRecognizer.SET_CARD_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "TMP]    Power Sink";  // partial with spaces
+        matcher = DeckRecognizer.SET_CARD_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        validRequest = "(TMP Power Sink";  // only left-handside
+        matcher = DeckRecognizer.SET_CARD_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+    }
+
+    @Test void testInvalidMatchSetCardLine(){
+        // == Invalid Cases for this REGEX
+        // Remeber: this rex *always* expects a Set Code!
+
+        String invalidRequest = "Power Sink";  // mind last space
+        Matcher matcher = DeckRecognizer.SET_CARD_PATTERN.matcher(invalidRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Sink");
+        assertNotNull(matcher.group(DeckRecognizer.REGRP_SET));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "Power");
+
+        invalidRequest = "22 Power Sink";  // mind last space
+        matcher = DeckRecognizer.SET_CARD_PATTERN.matcher(invalidRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "22");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Sink");
+        assertNotNull(matcher.group(DeckRecognizer.REGRP_SET));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "Power");
+    }
+
+    //=== Card-Set-CollectorNumber request
+    @Test void testMatchFullCardSetRequest(){
+        String validRequest = "1 Power Sink TMP 78";
+        Matcher matcher = DeckRecognizer.CARD_SET_COLLNO_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "1");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "78");
+
+        validRequest = "1 Power Sink|TMP 78";
+        matcher = DeckRecognizer.CARD_SET_COLLNO_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "1");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "78");
+
+        validRequest = "1 Power Sink (TMP) 78";  // MTG Arena alike
+        matcher = DeckRecognizer.CARD_SET_COLLNO_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "1");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink ");  // TRIM
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "78");
+
+        validRequest = "Power Sink (TMP) 78";  // MTG Arena alike
+        matcher = DeckRecognizer.CARD_SET_COLLNO_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink ");  // TRIM
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "78");
+    }
+
+    @Test void testInvalidMatchFullCardSetRequest(){
+        // NOTE: this will be matcher by another pattern
+        String invalidRequest = "1 Power Sink TMP";  // missing collector number
+        Matcher matcher = DeckRecognizer.CARD_SET_COLLNO_PATTERN.matcher(invalidRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "1");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "TMP");
+
+        // Note: this will be matched by another pattern
+        invalidRequest = "1 TMP Power Sink";
+        matcher = DeckRecognizer.CARD_SET_COLLNO_PATTERN.matcher(invalidRequest);
+        assertFalse(matcher.matches());
+    }
+
+    // === Set-Card-CollectorNumber Request
+    @Test void testMatchFullSetCardRequest(){
+        String validRequest = "1 TMP Power Sink 78";
+        Matcher matcher = DeckRecognizer.SET_CARD_COLLNO_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "1");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "78");
+
+        validRequest = "4x TMP Power Sink 78";
+        matcher = DeckRecognizer.SET_CARD_COLLNO_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "4");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "78");
+
+        validRequest = "TMP Power Sink 78";
+        matcher = DeckRecognizer.SET_CARD_COLLNO_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "78");
+
+        validRequest = "(TMP) Power Sink 78";
+        matcher = DeckRecognizer.SET_CARD_COLLNO_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "78");
+
+        validRequest = "TMP|Power Sink 78";
+        matcher = DeckRecognizer.SET_CARD_COLLNO_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "78");
+
+        validRequest = "[TMP] Power Sink 78";
+        matcher = DeckRecognizer.SET_CARD_COLLNO_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "78");
+
+        validRequest = "TMP| Power Sink 78";  // extra space
+        matcher = DeckRecognizer.SET_CARD_COLLNO_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "78");
+
+        validRequest = "tmp Power Sink 78";  // set code lowercase
+        matcher = DeckRecognizer.SET_CARD_COLLNO_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "tmp");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "78");
+
+        validRequest = "(TMP} Power Sink 78";  // mixed delimiters - still valid :D
+        matcher = DeckRecognizer.SET_CARD_COLLNO_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "78");
+    }
+
+    @Test void testInvalidMatchFullSetCardRequest(){
+        System.out.println(DeckRecognizer.REX_FULL_REQUEST_SET_CARD);
+        // NOTE: this will be matcher by another pattern
+        String invalidRequest = "1 Power Sink TMP";  // missing collector number
+        Matcher matcher = DeckRecognizer.SET_CARD_COLLNO_PATTERN.matcher(invalidRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "1");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "Power");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "TMP");
+
+        // Note: this will be matched by another pattern
+        invalidRequest = "1 TMP Power Sink";
+        matcher = DeckRecognizer.SET_CARD_COLLNO_PATTERN.matcher(invalidRequest);
+        assertFalse(matcher.matches());
+    }
+
+    @Test void testCrossRexForDifferentLineRequests(){
+        String cardRequest = "4x Power Sink TMP 78";
+        assertTrue(DeckRecognizer.CARD_SET_COLLNO_PATTERN.matcher(cardRequest).matches());
+        assertTrue(DeckRecognizer.SET_CARD_COLLNO_PATTERN.matcher(cardRequest).matches());
+        assertTrue(DeckRecognizer.CARD_SET_PATTERN.matcher(cardRequest).matches());
+        assertTrue(DeckRecognizer.SET_CARD_PATTERN.matcher(cardRequest).matches());
+
+        cardRequest = "4x TMP Power Sink 78";
+        assertTrue(DeckRecognizer.CARD_SET_COLLNO_PATTERN.matcher(cardRequest).matches());
+        assertTrue(DeckRecognizer.SET_CARD_COLLNO_PATTERN.matcher(cardRequest).matches());
+        assertTrue(DeckRecognizer.CARD_SET_PATTERN.matcher(cardRequest).matches());
+        assertTrue(DeckRecognizer.SET_CARD_PATTERN.matcher(cardRequest).matches());
+
+        cardRequest = "4x Power Sink TMP";
+        assertTrue(DeckRecognizer.CARD_SET_COLLNO_PATTERN.matcher(cardRequest).matches());
+        assertTrue(DeckRecognizer.SET_CARD_COLLNO_PATTERN.matcher(cardRequest).matches());
+        assertTrue(DeckRecognizer.CARD_SET_PATTERN.matcher(cardRequest).matches());
+        assertTrue(DeckRecognizer.SET_CARD_PATTERN.matcher(cardRequest).matches());
+
+        cardRequest = "4x TMP Power Sink";
+        // ONLY CASES IN WHICH THIS WILL FAIL
+        assertFalse(DeckRecognizer.CARD_SET_COLLNO_PATTERN.matcher(cardRequest).matches());
+        assertFalse(DeckRecognizer.SET_CARD_COLLNO_PATTERN.matcher(cardRequest).matches());
+
+        assertTrue(DeckRecognizer.CARD_SET_PATTERN.matcher(cardRequest).matches());
+        assertTrue(DeckRecognizer.SET_CARD_PATTERN.matcher(cardRequest).matches());
+    }
+
+    // === Card Only Request
+
+    @Test void testMatchCardOnlyRequest(){
+        String validRequest = "4x Power Sink";
+        Matcher matcher = DeckRecognizer.CARD_ONLY_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "4");
+
+        validRequest = "Power Sink";
+        matcher = DeckRecognizer.CARD_ONLY_PATTERN.matcher(validRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink");
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+
+        String invalidRequest = "";
+        matcher = DeckRecognizer.CARD_ONLY_PATTERN.matcher(invalidRequest);
+        assertFalse(matcher.matches());
+
+        invalidRequest = "TMP";  // set code (there is no way for this to not match)
+        matcher = DeckRecognizer.CARD_ONLY_PATTERN.matcher(invalidRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "TMP");
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+
+        invalidRequest = "78";  // collector number (there is no way for this to not match)
+        matcher = DeckRecognizer.CARD_ONLY_PATTERN.matcher(invalidRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "78");
+        assertNull(matcher.group(DeckRecognizer.REGRP_CARDNO));
+    }
+
+    @Test void testMatchFoilCardRequest(){
+        // card-set-collnr
+        String foilRequest = "4x Power Sink+ (TMP) 78";
+        Pattern target = DeckRecognizer.CARD_SET_COLLNO_PATTERN;
+        Matcher matcher = target.matcher(foilRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "4");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink+ ");  // TRIM
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "78");
+
+        // Set-card-collnr
+        foilRequest = "4x (TMP) Power Sink+ 78";
+        target = DeckRecognizer.SET_CARD_COLLNO_PATTERN;
+        matcher = target.matcher(foilRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "4");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink+");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "78");
+
+        // set-card
+        foilRequest = "4x (TMP) Power Sink+";
+        target = DeckRecognizer.SET_CARD_PATTERN;
+        matcher = target.matcher(foilRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "4");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink+");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        // card-set
+        foilRequest = "4x Power Sink+ (TMP)";
+        target = DeckRecognizer.CARD_SET_PATTERN;
+        matcher = target.matcher(foilRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "4");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink+ "); // TRIM
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "TMP");
+
+        // card-only
+        foilRequest = "4x Power Sink+";
+        target = DeckRecognizer.CARD_ONLY_PATTERN;
+        matcher = target.matcher(foilRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "4");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Power Sink+");
+    }
+
+    @Test void testRecogniseCardToken(){
+        StaticData magicDb = FModel.getMagicDb();
+        CardDb db = magicDb.getCommonCards();
+        CardDb altDb = magicDb.getVariantCards();
+        DeckRecognizer recognizer = new DeckRecognizer(db, altDb);
+
+        String lineRequest = "4x Power Sink+ (TMP) 78";
+        Token cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        PaperCard tokenCard = cardToken.getCard();
+        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(tokenCard.getName(), "Power Sink");
+        assertTrue(tokenCard.isFoil());
+        assertEquals(tokenCard.getEdition(), "TMP");
+        assertEquals(tokenCard.getCollectorNumber(), "78");
+
+        lineRequest = "4x Power Sink TMP 78";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        tokenCard = cardToken.getCard();
+        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(tokenCard.getName(), "Power Sink");
+        assertFalse(tokenCard.isFoil());
+        assertEquals(tokenCard.getEdition(), "TMP");
+        assertEquals(tokenCard.getCollectorNumber(), "78");
+
+        lineRequest = "4x TMP Power Sink 78";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        tokenCard = cardToken.getCard();
+        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(tokenCard.getName(), "Power Sink");
+        assertFalse(tokenCard.isFoil());
+        assertEquals(tokenCard.getEdition(), "TMP");
+        assertEquals(tokenCard.getCollectorNumber(), "78");
+
+        lineRequest = "4x TMP Power Sink";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        tokenCard = cardToken.getCard();
+        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(tokenCard.getName(), "Power Sink");
+        assertFalse(tokenCard.isFoil());
+        assertEquals(tokenCard.getEdition(), "TMP");
+        assertEquals(tokenCard.getCollectorNumber(), "78");
+
+        lineRequest = "4x Power Sink TMP";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        tokenCard = cardToken.getCard();
+        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(tokenCard.getName(), "Power Sink");
+        assertFalse(tokenCard.isFoil());
+        assertEquals(tokenCard.getEdition(), "TMP");
+        assertEquals(tokenCard.getCollectorNumber(), "78");
+
+        lineRequest = "Power Sink TMP";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        tokenCard = cardToken.getCard();
+        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(tokenCard.getName(), "Power Sink");
+        assertFalse(tokenCard.isFoil());
+        assertEquals(tokenCard.getEdition(), "TMP");
+        assertEquals(tokenCard.getCollectorNumber(), "78");
+
+        lineRequest = "[TMP] Power Sink";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        tokenCard = cardToken.getCard();
+        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(tokenCard.getName(), "Power Sink");
+        assertFalse(tokenCard.isFoil());
+        assertEquals(tokenCard.getEdition(), "TMP");
+        assertEquals(tokenCard.getCollectorNumber(), "78");
+
+        // Relax Set Preference
+        assertEquals(db.getCardArtPreference(), CardDb.CardArtPreference.LATEST_ART_ALL_EDITIONS);
+
+        lineRequest = "4x Power Sink";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        tokenCard = cardToken.getCard();
+        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(tokenCard.getName(), "Power Sink");
+        assertFalse(tokenCard.isFoil());
+        assertEquals(tokenCard.getEdition(), "VMA");
+
+        lineRequest = "Power Sink";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        tokenCard = cardToken.getCard();
+        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(tokenCard.getName(), "Power Sink");
+        assertFalse(tokenCard.isFoil());
+        assertEquals(tokenCard.getEdition(), "VMA");
+    }
+
+    @Test void testRecognisingCardFromSetUsingAlternateCode(){
+        StaticData magicDb = FModel.getMagicDb();
+        CardDb db = magicDb.getCommonCards();
+        CardDb altDb = magicDb.getVariantCards();
+        DeckRecognizer recognizer = new DeckRecognizer(db, altDb);
+
+        String lineRequest = "4x Power Sink+ TE 78";
+        Token cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        PaperCard tokenCard = cardToken.getCard();
+        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(tokenCard.getName(), "Power Sink");
+        assertTrue(tokenCard.isFoil());
+        assertEquals(tokenCard.getEdition(), "TMP");
+        assertEquals(tokenCard.getCollectorNumber(), "78");
+    }
+
+    @Test void testSingleWordCardNameMatchesCorrectly(){
+        StaticData magicDb = FModel.getMagicDb();
+        CardDb db = magicDb.getCommonCards();
+        CardDb altDb = magicDb.getVariantCards();
+        DeckRecognizer recognizer = new DeckRecognizer(db, altDb);
+
+        String lineRequest = "2x Counterspell ICE";
+        Token cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        PaperCard tokenCard = cardToken.getCard();
+        assertEquals(cardToken.getNumber(), 2);
+        assertEquals(tokenCard.getName(), "Counterspell");
+        assertEquals(tokenCard.getEdition(), "ICE");
+
+        // Remove Set code
+        assertEquals(db.getCardArtPreference(), CardDb.CardArtPreference.LATEST_ART_ALL_EDITIONS);
+        lineRequest = "2x Counterspell";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        tokenCard = cardToken.getCard();
+        assertEquals(cardToken.getNumber(), 2);
+        assertEquals(tokenCard.getName(), "Counterspell");
+        assertEquals(tokenCard.getEdition(), "MH2");
+
+    }
+
+    @Test void testPassingInArtIndexRatherThanCollectorNumber(){
+        StaticData magicDb = FModel.getMagicDb();
+        CardDb db = magicDb.getCommonCards();
+        CardDb altDb = magicDb.getVariantCards();
+        DeckRecognizer recognizer = new DeckRecognizer(db, altDb);
+
+        String lineRequest = "20x Mountain MIR 3";
+        Token cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        PaperCard tokenCard = cardToken.getCard();
+        assertEquals(cardToken.getNumber(), 20);
+        assertEquals(tokenCard.getName(), "Mountain");
+        assertEquals(tokenCard.getEdition(), "MIR");
+        assertEquals(tokenCard.getArtIndex(), 3);
+        assertEquals(tokenCard.getCollectorNumber(), "345");
+    }
+
+    @Test void testCollectorNumberIsNotConfusedAsArtIndexInstead(){
+        StaticData magicDb = FModel.getMagicDb();
+        CardDb db = magicDb.getCommonCards();
+        CardDb altDb = magicDb.getVariantCards();
+        DeckRecognizer recognizer = new DeckRecognizer(db, altDb);
+
+        String lineRequest = "2x Auspicious Ancestor MIR 3";
+        Token cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        PaperCard tokenCard = cardToken.getCard();
+        assertEquals(cardToken.getNumber(), 2);
+        assertEquals(tokenCard.getName(), "Auspicious Ancestor");
+        assertEquals(tokenCard.getEdition(), "MIR");
+        assertEquals(tokenCard.getArtIndex(), 1);
+        assertEquals(tokenCard.getCollectorNumber(), "3");
+    }
+
+    @Test void testRequestingCardWithWrongCollectorNumberReturnsUnknownCard(){
+        StaticData magicDb = FModel.getMagicDb();
+        CardDb db = magicDb.getCommonCards();
+        CardDb altDb = magicDb.getVariantCards();
+        DeckRecognizer recognizer = new DeckRecognizer(db, altDb);
+
+        String lineRequest = "2x Power Sink MIR 3";
+        Token cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.UNKNOWN_CARD_REQUEST);
+        assertNull(cardToken.getCard());
+    }
+
+    @Test void testRequestingCardFromTheWrongSetReturnsUnknownCard(){
+        StaticData magicDb = FModel.getMagicDb();
+        CardDb db = magicDb.getCommonCards();
+        CardDb altDb = magicDb.getVariantCards();
+        DeckRecognizer recognizer = new DeckRecognizer(db, altDb);
+
+        String lineRequest = "2x Counterspell FEM";
+        Token cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.UNKNOWN_CARD_REQUEST);
+        assertNull(cardToken.getCard());
+    }
+
+    @Test void testRequestingCardFromNonExistingSetReturnsUnknownCard(){
+        StaticData magicDb = FModel.getMagicDb();
+        CardDb db = magicDb.getCommonCards();
+        CardDb altDb = magicDb.getVariantCards();
+        DeckRecognizer recognizer = new DeckRecognizer(db, altDb);
+
+        String lineRequest = "2x Counterspell BOU";
+        Token cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.UNKNOWN_CARD_REQUEST);
+        assertNull(cardToken.getCard());
+    }
+
+    @Test void testRequestingCardWithRestrictionsOnSetsFromGameFormat(){
+        StaticData magicDb = FModel.getMagicDb();
+        CardDb db = magicDb.getCommonCards();
+        CardDb altDb = magicDb.getVariantCards();
+        DeckRecognizer recognizer = new DeckRecognizer(db, altDb);
+        // Setting Fantasy Constructed Game Format: Urza's Block Format
+        List<String> allowedSets = Arrays.asList("USG", "ULG", "UDS", "PUDS", "PULG", "PUSG");
+        recognizer.setGameFormatConstraint(allowedSets);
+
+        String lineRequest = "2x Counterspell ICE";
+        Token cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.ILLEGAL_CARD_REQUEST);
+        assertNull(cardToken.getCard());
+
+        lineRequest = "2x Counterspell";  // It does not exist any Counterspell in Urza's block
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.ILLEGAL_CARD_REQUEST);
+        assertNull(cardToken.getCard());
+    }
+
+    @Test void testRequestingCardWithRestrictionsOnDeckFormat(){
+        StaticData magicDb = FModel.getMagicDb();
+        CardDb db = magicDb.getCommonCards();
+        CardDb altDb = magicDb.getVariantCards();
+        DeckRecognizer recognizer = new DeckRecognizer(db, altDb);
+
+        String lineRequest = "Ancestral Recall";
+        Token cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        PaperCard ancestralCard = cardToken.getCard();
+        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(ancestralCard.getName(), "Ancestral Recall");
+        assertEquals(db.getCardArtPreference(), CardDb.CardArtPreference.LATEST_ART_ALL_EDITIONS);
+        assertEquals(ancestralCard.getEdition(), "VMA");
+
+        recognizer.setDeckFormatConstraint(DeckFormat.TinyLeaders);
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.ILLEGAL_CARD_REQUEST);
+        assertNull(cardToken.getCard());
+    }
+
+    @Test void testRequestingCardWithReleaseDateConstraints(){
+        StaticData magicDb = FModel.getMagicDb();
+        CardDb db = magicDb.getCommonCards();
+        CardDb altDb = magicDb.getVariantCards();
+        DeckRecognizer recognizer = new DeckRecognizer(db, altDb);
+        recognizer.setDateConstraint(2002, 1);
+        assertEquals(db.getCardArtPreference(), CardDb.CardArtPreference.LATEST_ART_ALL_EDITIONS);
+
+        String lineRequest = "Ancestral Recall";
+        Token cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        PaperCard ancestralCard = cardToken.getCard();
+        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(ancestralCard.getName(), "Ancestral Recall");
+        assertEquals(db.getCardArtPreference(), CardDb.CardArtPreference.LATEST_ART_ALL_EDITIONS);
+        assertEquals(ancestralCard.getEdition(), "2ED");
+
+        // Counterspell editions
+        lineRequest = "Counterspell";
+        recognizer.setDateConstraint(1999, 10);
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        PaperCard counterSpellCard = cardToken.getCard();
+        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(counterSpellCard.getName(), "Counterspell");
+        assertEquals(counterSpellCard.getEdition(), "MMQ");
+    }
+
+    @Test void testInvalidCardRequestWhenReleaseDAteConstraintsAreUp(){
+        StaticData magicDb = FModel.getMagicDb();
+        CardDb db = magicDb.getCommonCards();
+        CardDb altDb = magicDb.getVariantCards();
+        DeckRecognizer recognizer = new DeckRecognizer(db, altDb);
+
+        assertEquals(db.getCardArtPreference(), CardDb.CardArtPreference.LATEST_ART_ALL_EDITIONS);
+
+        // First run without constraint to check that it's a valid request
+        String lineRequest = "Counterspell|MH2";
+        Token cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        PaperCard counterSpellCard = cardToken.getCard();
+        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(counterSpellCard.getName(), "Counterspell");
+        assertEquals(counterSpellCard.getEdition(), "MH2");
+
+        recognizer.setDateConstraint(1999, 10);
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.INVALID_CARD_REQUEST);
+        assertNull(cardToken.getCard());
+    }
+}