// Path: src/main/java/com/natpryce/worktorule/IssueTracker.java
// public interface IssueTracker {
//     /**
//      * Reports if the issue with the given ID is open.
//      *
//      * @param issueId the ID of the issue to query
//      * @return true if the issue is open, false otherwise
//      * @throws IOException if communication with the issue tracker failed
//      */
//     boolean isOpen(String issueId) throws IOException;
// }
// 
// Path: src/main/java/com/natpryce/worktorule/http/HttpConnectionSetting.java
// public interface HttpConnectionSetting {
//     void applyTo(HttpURLConnection cx);
// }
// 
// Path: src/main/java/com/natpryce/worktorule/internal/JsonHttp.java
// public class JsonHttp {
//     private final ObjectMapper objectMapper = new ObjectMapper();
//     private final String acceptedContentType;
//     private final HttpConnectionSetting[] connectionSettings;
// 
//     public JsonHttp(String acceptedContentType, HttpConnectionSetting... connectionSettings) {
//         this.acceptedContentType = acceptedContentType;
//         this.connectionSettings = connectionSettings;
//     }
// 
//     public JsonNode getJson(URI uri) throws IOException {
//         return getJson(uri.toURL());
//     }
// 
//     public JsonNode getJson(URL url) throws IOException {
//         HttpURLConnection cx = (HttpURLConnection) url.openConnection();
//         cx.setRequestProperty("Accept", acceptedContentType);
//         cx.setRequestProperty("User-Agent", getClass().getName());
//         for (HttpConnectionSetting connectionSetting : connectionSettings) {
//             connectionSetting.applyTo(cx);
//         }
// 
//         int responseCode = cx.getResponseCode();
//         if (responseCode >= 300) {
//             throw new IOException("request to " + url + " failed with status " + responseCode);
//         }
// 
//         return parseJson(cx);
//     }
// 
//     private JsonNode parseJson(HttpURLConnection cx) throws IOException {
//         Reader r = new InputStreamReader(new BufferedInputStream(cx.getInputStream()), parseCharset(cx));
//         try {
//             return objectMapper.readTree(r);
//         } finally {
//             r.close();
//         }
//     }
// 
//     private String parseCharset(MimeType contentType) {
//         return Optional.ofNullable(contentType.getParameter("charset")).orElse("utf-8");
//     }
// 
//     private String parseCharset(HttpURLConnection cx) throws IOException {
//         return parseCharset(parseContentType(cx));
//     }
// 
//     private MimeType parseContentType(HttpURLConnection cx) throws IOException {
//         String contentType = cx.getContentType();
//         try {
//             return new MimeType(contentType);
//         } catch (MimeTypeParseException e) {
//             throw new IOException("failed to parse content type: " + contentType, e);
//         }
//     }
// }


import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.natpryce.worktorule.IssueTracker;
import com.natpryce.worktorule.http.HttpConnectionSetting;
import com.natpryce.worktorule.internal.JsonHttp;
import java.io.IOException;
import java.util.Optional;
import java.util.Set;
import static com.fasterxml.jackson.databind.node.JsonNodeType.BOOLEAN;
import static com.fasterxml.jackson.databind.node.JsonNodeType.STRING;
package com.natpryce.worktorule.issues.trello;




public class TrelloCards implements IssueTracker {
    private final Set<String> doneListIds;
    private final JsonHttp jsonHttp;
    private final TrelloCardFieldUriScheme cardClosedUriScheme;
    private final TrelloCardFieldUriScheme cardListIdUriScheme;

    public TrelloCards(String apiKey, Optional<String> userToken, Set<String> closedIssueListIds, HttpConnectionSetting ... connectionSettings) {
        this.jsonHttp = new JsonHttp("application/json", connectionSettings);
        this.cardClosedUriScheme = new TrelloCardFieldUriScheme(apiKey, userToken,
                "https://api.trello.com/1/card/{cardId}/closed{?key,token}");
        this.cardListIdUriScheme = new TrelloCardFieldUriScheme(apiKey, userToken,
                "https://api.trello.com/1/card/{cardId}/idList{?key,token}");
        this.doneListIds = closedIssueListIds;
    }

    @Override
    public boolean isOpen(String cardId) throws IOException {
        return !cardIsClosed(cardId) && !doneListIds.contains(cardListId(cardId));
    }

    private String cardListId(String cardId) throws IOException {
        return getCardField(cardId, cardListIdUriScheme, STRING).textValue();
    }

    private JsonNode getCardField(String cardId, TrelloCardFieldUriScheme uriScheme, JsonNodeType nodeType) throws IOException {
        JsonNode valueNode = jsonHttp.getJson(uriScheme.uriForIssue(cardId)).findPath("_value");
        if (valueNode.getNodeType() != nodeType) {
            throw new JsonMappingException("unexpected JSON structure");
        }
        return valueNode;
    }

// Path: src/main/java/com/natpryce/worktorule/IssueTracker.java
// public interface IssueTracker {
//     /**
//      * Reports if the issue with the given ID is open.
//      *
//      * @param issueId the ID of the issue to query
//      * @return true if the issue is open, false otherwise
//      * @throws IOException if communication with the issue tracker failed
//      */
//     boolean isOpen(String issueId) throws IOException;
// }
// 
// Path: src/main/java/com/natpryce/worktorule/http/HttpConnectionSetting.java
// public interface HttpConnectionSetting {
//     void applyTo(HttpURLConnection cx);
// }
// 
// Path: src/main/java/com/natpryce/worktorule/internal/JsonHttp.java
// public class JsonHttp {
//     private final ObjectMapper objectMapper = new ObjectMapper();
//     private final String acceptedContentType;
//     private final HttpConnectionSetting[] connectionSettings;
// 
//     public JsonHttp(String acceptedContentType, HttpConnectionSetting... connectionSettings) {
//         this.acceptedContentType = acceptedContentType;
//         this.connectionSettings = connectionSettings;
//     }
// 
//     public JsonNode getJson(URI uri) throws IOException {
//         return getJson(uri.toURL());
//     }
// 
//     public JsonNode getJson(URL url) throws IOException {
//         HttpURLConnection cx = (HttpURLConnection) url.openConnection();
//         cx.setRequestProperty("Accept", acceptedContentType);
//         cx.setRequestProperty("User-Agent", getClass().getName());
//         for (HttpConnectionSetting connectionSetting : connectionSettings) {
//             connectionSetting.applyTo(cx);
//         }
// 
//         int responseCode = cx.getResponseCode();
//         if (responseCode >= 300) {
//             throw new IOException("request to " + url + " failed with status " + responseCode);
//         }
// 
//         return parseJson(cx);
//     }
// 
//     private JsonNode parseJson(HttpURLConnection cx) throws IOException {
//         Reader r = new InputStreamReader(new BufferedInputStream(cx.getInputStream()), parseCharset(cx));
//         try {
//             return objectMapper.readTree(r);
//         } finally {
//             r.close();
//         }
//     }
// 
//     private String parseCharset(MimeType contentType) {
//         return Optional.ofNullable(contentType.getParameter("charset")).orElse("utf-8");
//     }
// 
//     private String parseCharset(HttpURLConnection cx) throws IOException {
//         return parseCharset(parseContentType(cx));
//     }
// 
//     private MimeType parseContentType(HttpURLConnection cx) throws IOException {
//         String contentType = cx.getContentType();
//         try {
//             return new MimeType(contentType);
//         } catch (MimeTypeParseException e) {
//             throw new IOException("failed to parse content type: " + contentType, e);
//         }
//     }
// }


// Path: src/main/java/com/natpryce/worktorule/issues/trello/TrelloCards.java
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.natpryce.worktorule.IssueTracker;
import com.natpryce.worktorule.http.HttpConnectionSetting;
import com.natpryce.worktorule.internal.JsonHttp;
import java.io.IOException;
import java.util.Optional;
import java.util.Set;
import static com.fasterxml.jackson.databind.node.JsonNodeType.BOOLEAN;
import static com.fasterxml.jackson.databind.node.JsonNodeType.STRING;

package com.natpryce.worktorule.issues.trello;




public class TrelloCards implements IssueTracker {
    private final Set<String> doneListIds;
    private final JsonHttp jsonHttp;
    private final TrelloCardFieldUriScheme cardClosedUriScheme;
    private final TrelloCardFieldUriScheme cardListIdUriScheme;

    public TrelloCards(String apiKey, Optional<String> userToken, Set<String> closedIssueListIds, HttpConnectionSetting ... connectionSettings) {
        this.jsonHttp = new JsonHttp("application/json", connectionSettings);
        this.cardClosedUriScheme = new TrelloCardFieldUriScheme(apiKey, userToken,
                "https://api.trello.com/1/card/{cardId}/closed{?key,token}");
        this.cardListIdUriScheme = new TrelloCardFieldUriScheme(apiKey, userToken,
                "https://api.trello.com/1/card/{cardId}/idList{?key,token}");
        this.doneListIds = closedIssueListIds;
    }

    @Override
    public boolean isOpen(String cardId) throws IOException {
        return !cardIsClosed(cardId) && !doneListIds.contains(cardListId(cardId));
    }

    private String cardListId(String cardId) throws IOException {
        return getCardField(cardId, cardListIdUriScheme, STRING).textValue();
    }

    private JsonNode getCardField(String cardId, TrelloCardFieldUriScheme uriScheme, JsonNodeType nodeType) throws IOException {
        JsonNode valueNode = jsonHttp.getJson(uriScheme.uriForIssue(cardId)).findPath("_value");
        if (valueNode.getNodeType() != nodeType) {
            throw new JsonMappingException("unexpected JSON structure");
        }
        return valueNode;
    }

    private boolean cardIsClosed(String cardId) throws IOException {